Index: pkg/polymer/lib/src/build/script_compactor.dart |
diff --git a/pkg/polymer/lib/src/build/script_compactor.dart b/pkg/polymer/lib/src/build/script_compactor.dart |
index e869a2cb55c253bf7ffcc6f26bde5078241dcec8..a9a4d595cba2c24af36233bdf5ebc9dd0aa9be22 100644 |
--- a/pkg/polymer/lib/src/build/script_compactor.dart |
+++ b/pkg/polymer/lib/src/build/script_compactor.dart |
@@ -8,9 +8,15 @@ library polymer.src.build.script_compactor; |
import 'dart:async'; |
import 'dart:convert'; |
+import 'package:analyzer/src/generated/ast.dart'; |
+import 'package:analyzer/src/generated/error.dart'; |
+import 'package:analyzer/src/generated/java_core.dart' show CharSequence; |
+import 'package:analyzer/src/generated/parser.dart'; |
+import 'package:analyzer/src/generated/scanner.dart'; |
import 'package:barback/barback.dart'; |
import 'package:html5lib/parser.dart' show parseFragment; |
import 'package:path/path.dart' as path; |
+import 'package:source_maps/span.dart' show SourceFile; |
import 'code_extractor.dart'; // import just for documentation. |
import 'common.dart'; |
@@ -77,6 +83,7 @@ class ScriptCompactor extends Transformer with PolymerTransformer { |
return null; |
} |
+ // Emit the bootstrap .dart file |
var bootstrapId = id.addExtension('_bootstrap.dart'); |
mainScriptTag.attributes['src'] = |
path.url.basename(bootstrapId.path); |
@@ -92,20 +99,187 @@ class ScriptCompactor extends Transformer with PolymerTransformer { |
buffer..write('\n') |
..writeln('void main() {') |
- ..writeln(' configureForDeployment([') |
- ..writeAll(urls.map((url) => " '$url',\n")) |
- ..writeln(' ]);') |
- ..writeln(' i${i - 1}.main();') |
- ..writeln('}'); |
- |
- transform.addOutput(new Asset.fromString( |
- bootstrapId, buffer.toString())); |
- transform.addOutput(new Asset.fromString(id, document.outerHtml)); |
+ ..writeln(' configureForDeployment(['); |
+ |
+ // Inject @CustomTag and @initMethod initializations for each library |
+ // that is sourced in a script tag. |
+ i = 0; |
+ return Future.forEach(libraries, (lib) { |
+ return _initializersOf(lib, transform, logger).then((initializers) { |
+ for (var init in initializers) { |
+ var code = init.asCode('i$i'); |
+ buffer.write(" $code,\n"); |
+ } |
+ i++; |
+ }); |
+ }).then((_) { |
+ buffer..writeln(' ]);') |
+ ..writeln(' i${urls.length - 1}.main();') |
+ ..writeln('}'); |
+ |
+ transform.addOutput(new Asset.fromString( |
+ bootstrapId, buffer.toString())); |
+ transform.addOutput(new Asset.fromString(id, document.outerHtml)); |
+ }); |
}); |
}); |
} |
+ |
+ /** |
+ * Computes the initializers of [dartLibrary]. That is, a closure that calls |
+ * Polymer.register for each @CustomTag, and any public top-level methods |
+ * labeled with @initMethod. |
+ */ |
+ Future<List<_Initializer>> _initializersOf( |
+ AssetId dartLibrary, Transform transform, TransformLogger logger) { |
+ var initializers = []; |
+ return transform.readInputAsString(dartLibrary).then((code) { |
+ var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); |
+ var unit = _parseCompilationUnit(code); |
+ |
+ return Future.forEach(unit.directives, (directive) { |
+ // Include anything from parts. |
+ if (directive is PartDirective) { |
+ var targetId = resolve(dartLibrary, directive.uri.stringValue, |
+ logger, _getSpan(file, directive)); |
+ return _initializersOf(targetId, transform, logger) |
+ .then(initializers.addAll); |
+ } |
+ |
+ // Similarly, include anything from exports except what's filtered by |
+ // the show/hide combinators. |
+ if (directive is ExportDirective) { |
+ var targetId = resolve(dartLibrary, directive.uri.stringValue, |
+ logger, _getSpan(file, directive)); |
+ return _initializersOf(targetId, transform, logger) |
+ .then((r) => _processExportDirective(directive, r, initializers)); |
+ } |
+ }).then((_) { |
+ // Scan the code for classes and top-level functions. |
+ for (var node in unit.declarations) { |
+ if (node is ClassDeclaration) { |
+ _processClassDeclaration(node, initializers, file, logger); |
+ } else if (node is FunctionDeclaration && |
+ node.metadata.any(_isInitMethodAnnotation)) { |
+ _processFunctionDeclaration(node, initializers, file, logger); |
+ } |
+ } |
+ return initializers; |
+ }); |
+ }); |
+ } |
+ |
+ static String _simpleUriForSource(AssetId source) => |
+ source.path.startsWith('lib/') |
+ ? 'package:${source.package}/${source.path.substring(4)}' : source.path; |
+ |
+ /** |
+ * Filter [exportedInitializers] according to [directive]'s show/hide |
+ * combinators and add the result to [initializers]. |
+ */ |
+ // TODO(sigmund): call the analyzer's resolver instead? |
+ static _processExportDirective(ExportDirective directive, |
+ List<_Initializer> exportedInitializers, |
+ List<_Initializer> initializers) { |
+ for (var combinator in directive.combinators) { |
+ if (combinator is ShowCombinator) { |
+ var show = combinator.shownNames.map((n) => n.name).toSet(); |
+ exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); |
+ } else if (combinator is HideCombinator) { |
+ var hide = combinator.hiddenNames.map((n) => n.name).toSet(); |
+ exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); |
+ } |
+ } |
+ initializers.addAll(exportedInitializers); |
+ } |
+ |
+ /** |
+ * Add an initializer to register [node] as a polymer element if it contains |
+ * an appropriate [CustomTag] annotation. |
+ */ |
+ static _processClassDeclaration(ClassDeclaration node, |
+ List<_Initializer> initializers, SourceFile file, |
+ TransformLogger logger) { |
+ for (var meta in node.metadata) { |
+ if (!_isCustomTagAnnotation(meta)) continue; |
+ var args = meta.arguments.arguments; |
+ if (args == null || args.length == 0) { |
+ logger.error('Missing argument in @CustomTag annotation', |
+ span: _getSpan(file, meta)); |
+ continue; |
+ } |
+ |
+ var tagName = args[0].stringValue; |
+ var typeName = node.name.name; |
+ if (typeName.startsWith('_')) { |
+ logger.error('@CustomTag is no longer supported on private ' |
+ 'classes: $tagName', span: _getSpan(file, node.name)); |
+ continue; |
+ } |
+ initializers.add(new _CustomTagInitializer(tagName, typeName)); |
+ } |
+ } |
+ |
+ /** a method initializer for [function]. */ |
+ static _processFunctionDeclaration(FunctionDeclaration function, |
+ List<_Initializer> initializers, SourceFile file, |
+ TransformLogger logger) { |
+ var name = function.name.name; |
+ if (name.startsWith('_')) { |
+ logger.error('@initMethod is no longer supported on private ' |
+ 'functions: $name', span: _getSpan(file, function.name)); |
+ return; |
+ } |
+ initializers.add(new _InitMethodInitializer(name)); |
+ } |
+} |
+ |
+/** Parse [code] using analyzer. */ |
+CompilationUnit _parseCompilationUnit(String code) { |
+ var errorListener = new _ErrorCollector(); |
+ var reader = new CharSequenceReader(new CharSequence(code)); |
+ var scanner = new Scanner(null, reader, errorListener); |
+ var token = scanner.tokenize(); |
+ var parser = new Parser(null, errorListener); |
+ return parser.parseCompilationUnit(token); |
} |
+class _ErrorCollector extends AnalysisErrorListener { |
+ final errors = <AnalysisError>[]; |
+ onError(error) => errors.add(error); |
+} |
+ |
+// TODO(sigmund): consider support for importing annotations with prefixes. |
+bool _isInitMethodAnnotation(Annotation node) => |
+ node.name.name == 'initMethod' && node.constructorName == null && |
+ node.arguments == null; |
+bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; |
+ |
+abstract class _Initializer { |
+ String get symbolName; |
+ String asCode(String prefix); |
+} |
+ |
+class _InitMethodInitializer implements _Initializer { |
+ String methodName; |
+ String get symbolName => methodName; |
+ _InitMethodInitializer(this.methodName); |
+ |
+ String asCode(String prefix) => "$prefix.$methodName"; |
+} |
+ |
+class _CustomTagInitializer implements _Initializer { |
+ String tagName; |
+ String typeName; |
+ String get symbolName => typeName; |
+ _CustomTagInitializer(this.tagName, this.typeName); |
+ |
+ String asCode(String prefix) => |
+ "() => Polymer.register('$tagName', $prefix.$typeName)"; |
+} |
+ |
+_getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); |
+ |
const MAIN_HEADER = """ |
library app_bootstrap; |