From fb499461602bb6ca507bec7fddbc7266f4bffadf Mon Sep 17 00:00:00 2001 From: Tim Blasi Date: Thu, 19 Feb 2015 12:00:09 -0800 Subject: [PATCH] feat(dart/transform) Allow ctor stubs to be tree shaken Change the method used to discover Directive annotated classes to ensure that the Dart code can be tree shaken. Closes #497 Closes $\x23736 --- .../src/transform/annotation_processor.dart | 53 +++--- modules/angular2/src/transform/codegen.dart | 16 +- .../src/transform/find_bootstrap.dart | 91 ++++++++++ modules/angular2/src/transform/logging.dart | 24 +++ modules/angular2/src/transform/options.dart | 13 +- modules/angular2/src/transform/resolvers.dart | 86 +++++++++ .../angular2/src/transform/transformer.dart | 55 ++++-- modules/angular2/src/transform/traversal.dart | 163 ++++++++---------- modules/angular2/test/transform/common.dart | 14 ++ .../html_entry_point_files/index.dart | 1 + .../transform/list_of_types_files/index.dart | 3 +- .../simple_annotation_files/index.dart | 3 +- .../transform/synthetic_ctor_files/index.dart | 3 +- .../test/transform/transform_test.dart | 34 +++- .../transform/two_annotations_files/bar.dart | 10 ++ .../expected/index.bootstrap.dart | 18 ++ .../two_annotations_files/index.dart | 8 + .../expected/index.bootstrap.dart | 4 +- .../test/transform/two_deps_files/index.dart | 3 +- modules/examples/pubspec.yaml | 1 + 20 files changed, 461 insertions(+), 142 deletions(-) create mode 100644 modules/angular2/src/transform/find_bootstrap.dart create mode 100644 modules/angular2/src/transform/logging.dart create mode 100644 modules/angular2/test/transform/common.dart create mode 100644 modules/angular2/test/transform/two_annotations_files/bar.dart create mode 100644 modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart create mode 100644 modules/angular2/test/transform/two_annotations_files/index.dart diff --git a/modules/angular2/src/transform/annotation_processor.dart b/modules/angular2/src/transform/annotation_processor.dart index b8399d8cc3..a04f31a6c2 100644 --- a/modules/angular2/src/transform/annotation_processor.dart +++ b/modules/angular2/src/transform/annotation_processor.dart @@ -1,8 +1,11 @@ library angular2.src.transform; import 'dart:collection' show Queue; +import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; +import 'resolvers.dart'; + /// Provides a mechanism for checking an element for the provided /// [_annotationClass] and reporting the resulting (element, annotation) pairs. class AnnotationMatcher { @@ -11,18 +14,19 @@ class AnnotationMatcher { /// All the annotations we have seen for each element final _seenAnnotations = new Map>(); - /// The class we are searching for to populate [initQueue]. - final ClassElement _annotationClass; + /// The classes we are searching for to populate [matchQueue]. + final Set _annotationClasses; - AnnotationMatcher(this._annotationClass); + AnnotationMatcher(this._annotationClasses); /// Records all [_annotationClass] annotations and the [element]s they apply to. - /// Returns [true] if 1) [element] is annotated with [_annotationClass] and - /// 2) ([element], [_annotationClass]) has not been seen previously. - bool processAnnotations(ClassElement element) { - var found = false; + /// Returns + List processAnnotations(ClassElement element) { + // Finding the node corresponding to [element] can be very expensive. + ClassDeclaration cachedNode = null; + + var result = []; element.metadata.where((ElementAnnotation meta) { - // Only process [_annotationClass]s. // TODO(tjblasi): Make this recognize non-ConstructorElement annotations. return meta.element is ConstructorElement && _isAnnotationMatch(meta.element.returnType); @@ -32,29 +36,36 @@ class AnnotationMatcher { .putIfAbsent(element, () => new Set()) .contains(meta); }).forEach((ElementAnnotation meta) { + if (cachedNode == null) { + cachedNode = element.node; + } + + var match = new AnnotationMatch(cachedNode, meta); _seenAnnotations[element].add(meta); - matchQueue.addLast(new AnnotationMatch(element, meta)); - found = true; + matchQueue.addLast(match); + result.add(match); }); - return found; + return result; } /// Whether [type], its superclass, or one of its interfaces matches [_annotationClass]. bool _isAnnotationMatch(InterfaceType type) { - if (type == null || type.element == null) return false; - if (type.element.type == _annotationClass.type) return true; - if (_isAnnotationMatch(type.superclass)) return true; - for (var interface in type.interfaces) { - if (_isAnnotationMatch(interface)) return true; - } - return false; + return _annotationClasses.any((el) => isAnnotationMatch(type, el)); } } -// Element/ElementAnnotation pair. +/// [ConstructorElement] / [ElementAnnotation] pair, where the Constructor class AnnotationMatch { - final Element element; + /// The resolved element corresponding to [node]. + final ClassElement element; + + /// The Ast node corresponding to the class definition. + final ClassDeclaration node; + + /// The resolved element for the matched annotation. final ElementAnnotation annotation; - AnnotationMatch(this.element, this.annotation); + AnnotationMatch(ClassDeclaration node, this.annotation) + : node = node, + element = node.element; } diff --git a/modules/angular2/src/transform/codegen.dart b/modules/angular2/src/transform/codegen.dart index 74e9ac233b..61b6fda209 100644 --- a/modules/angular2/src/transform/codegen.dart +++ b/modules/angular2/src/transform/codegen.dart @@ -28,7 +28,7 @@ class Context { } void error(String errorString) { - if (_logger == null) throw new Error(errorString); + if (_logger == null) throw new CodegenError(errorString); _logger.error(errorString); } @@ -44,6 +44,16 @@ class Context { } } +class CodegenError extends Error { + final String message; + CodegenError(this.message); + + @override + String toString() { + return 'Error generating Angular2 code: ${Error.safeToString(message)}'; + } +} + /// Object which [register]s [AnnotationMatch] objects for code generation. abstract class DirectiveRegistry { // Adds [entry] to the `registerType` calls which will be generated. @@ -107,6 +117,7 @@ _codegenImport(Context context, AssetId libraryId, AssetId entryPoint) { class _DirectiveRegistryImpl implements DirectiveRegistry { final Context _context; final StringBuffer _buffer = new StringBuffer(); + final Set _seen = new Set(); _DirectiveRegistryImpl(this._context); @@ -117,6 +128,9 @@ class _DirectiveRegistryImpl implements DirectiveRegistry { // Adds [entry] to the `registerType` calls which will be generated. void register(AnnotationMatch entry) { + if (_seen.contains(entry.node)) return; + _seen.add(entry.node); + var element = entry.element; var annotation = entry.annotation; diff --git a/modules/angular2/src/transform/find_bootstrap.dart b/modules/angular2/src/transform/find_bootstrap.dart new file mode 100644 index 0000000000..c6b9f4f52e --- /dev/null +++ b/modules/angular2/src/transform/find_bootstrap.dart @@ -0,0 +1,91 @@ +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:code_transformers/resolver.dart'; + +import 'resolvers.dart'; + +/// Finds all calls to the Angular2 [bootstrap] method defined in [library]. +/// This only searches the code defined in the file +// represented by [library], not `part`s, `import`s, `export`s, etc. +Set findBootstrapCalls( + Resolver resolver, LibraryElement library) { + var types = new Angular2Types(resolver); + if (types.bootstrapMethod == null) { + throw new ArgumentError( + 'Could not find symbol for ${bootstrapMethodName}.'); + } + var visitor = new _FindFunctionVisitor(types.bootstrapMethod); + + // TODO(kegluneq): Determine how to get nodes without querying Element#node. + // Root of file defining that library (main part). + library.definingCompilationUnit.node.accept(visitor); + + return new Set.from(visitor.functionCalls.map((MethodInvocation mi) { + var visitor = new _ParseBootstrapTypeVisitor(types); + if (mi.argumentList.arguments.isEmpty) { + throw new ArgumentError('No arguments provided to `bootstrap`.'); + } + mi.argumentList.arguments[0].accept(visitor); + if (visitor.bootstrapType == null) { + throw new UnsupportedError( + 'Failed to parse `bootstrap` call: ${mi.toSource()}'); + } + return new BootstrapCallInfo(mi, visitor.bootstrapType); + })); +} + +/// Information about a single call to Angular2's [bootstrap] method. +class BootstrapCallInfo { + /// The [AstNode] representing the call to [bootstrap]. + final MethodInvocation call; + + /// The type, which should be annotated as a [Component], which is the root + /// of the Angular2 app. + final ClassElement bootstrapType; + + BootstrapCallInfo(this.call, this.bootstrapType); +} + +/// Visitor that finds the Angular2 bootstrap component given [bootstrap]'s +/// first argument. +/// +/// This visitor does not recursively visit nodes in the Ast. +class _ParseBootstrapTypeVisitor extends SimpleAstVisitor { + ClassElement bootstrapType = null; + + final Angular2Types _types; + + _ParseBootstrapTypeVisitor(this._types); + + // TODO(kegluneq): Allow non-SimpleIdentifier expressions. + + @override + Object visitSimpleIdentifier(SimpleIdentifier e) { + bootstrapType = (e.bestElement as ClassElement); + if (!_types.isComponent(bootstrapType)) { + throw new ArgumentError('Class passed to `${bootstrapMethodName}` must ' + 'be a @${_types.componentAnnotation.name}'); + } + } +} + +/// Recursively visits all nodes in an Ast structure, recording all encountered +/// calls to the provided [FunctionElement]. +class _FindFunctionVisitor extends UnifyingAstVisitor { + final FunctionElement _target; + _FindFunctionVisitor(this._target); + + final Set functionCalls = new Set(); + + bool _isDesiredMethod(MethodInvocation node) { + return node.methodName.bestElement == _target; + } + + @override + Object visitMethodInvocation(MethodInvocation node) { + if (_isDesiredMethod(node)) { + functionCalls.add(node); + } + return super.visitMethodInvocation(node); + } +} diff --git a/modules/angular2/src/transform/logging.dart b/modules/angular2/src/transform/logging.dart new file mode 100644 index 0000000000..831e208f6b --- /dev/null +++ b/modules/angular2/src/transform/logging.dart @@ -0,0 +1,24 @@ +library angular2.src.transform.logging; + +import 'package:barback/barback.dart'; +import 'package:code_transformers/messages/build_logger.dart'; + +BuildLogger _logger; + +/// Prepares [logger] for use throughout the transformer. +void init(Transform t) { + if (_logger == null) { + _logger = new BuildLogger(t); + } else { + _logger.fine('Attempted to initialize logger multiple times.', + asset: t.primaryInput.id); + } +} + +/// The logger the transformer should use for messaging. +BuildLogger get logger { + if (_logger == null) { + throw new StateError('Logger never initialized.'); + } + return _logger; +} diff --git a/modules/angular2/src/transform/options.dart b/modules/angular2/src/transform/options.dart index 5b51a3a847..45ebbf96fb 100644 --- a/modules/angular2/src/transform/options.dart +++ b/modules/angular2/src/transform/options.dart @@ -2,12 +2,23 @@ library angular2.src.transform; import 'package:path/path.dart' as path; +/// Provides information necessary to transform an Angular2 app. class TransformerOptions { + /// The file where the application's call to [bootstrap] is. + // TODO(kegluenq): Allow multiple bootstrap entry points. + final String bootstrapEntryPoint; + + /// The Dart entry point, that is, where the initial call to [main] occurs. final String entryPoint; + + /// The path where we should generate code. final String newEntryPoint; + + /// The html file that includes [entryPoint]. final String htmlEntryPoint; - TransformerOptions(this.entryPoint, this.newEntryPoint, this.htmlEntryPoint); + TransformerOptions(this.bootstrapEntryPoint, this.entryPoint, + this.newEntryPoint, this.htmlEntryPoint); bool inSameTopLevelDir() { var expectedDir = path.split(htmlEntryPoint)[0]; diff --git a/modules/angular2/src/transform/resolvers.dart b/modules/angular2/src/transform/resolvers.dart index 7f23d606ad..45f02a27d6 100644 --- a/modules/angular2/src/transform/resolvers.dart +++ b/modules/angular2/src/transform/resolvers.dart @@ -1,5 +1,7 @@ library angular2.src.transform; +import 'package:barback/barback.dart'; +import 'package:analyzer/src/generated/element.dart'; import 'package:code_transformers/resolver.dart'; Resolvers createResolvers() { @@ -43,3 +45,87 @@ Resolvers createResolvers() { ''', }); } + +const bootstrapMethodName = 'bootstrap'; + +/// Provides resolved [Elements] for well-known Angular2 symbols. +class Angular2Types { + static Map _cache = {}; + static final _annotationsLibAssetId = + new AssetId('angular2', 'lib/src/core/annotations/annotations.dart'); + static final _applicationLibAssetId = + new AssetId('angular2', 'lib/src/core/application.dart'); + static final _templateLibAssetId = + new AssetId('angular2', 'lib/src/core/annotations/template.dart'); + + final Resolver _resolver; + FunctionElement _bootstrapMethod; + + Angular2Types._internal(this._resolver); + + factory Angular2Types(Resolver resolver) { + return _cache.putIfAbsent( + resolver, () => new Angular2Types._internal(resolver)); + } + + LibraryElement get annotationsLib => + _resolver.getLibrary(_annotationsLibAssetId); + + ClassElement get directiveAnnotation => + _getTypeSafe(annotationsLib, 'Directive'); + + ClassElement get componentAnnotation => + _getTypeSafe(annotationsLib, 'Component'); + + ClassElement get decoratorAnnotation => + _getTypeSafe(annotationsLib, 'Decorator'); + + LibraryElement get templateLib => _resolver.getLibrary(_templateLibAssetId); + + ClassElement get templateAnnotation => _getTypeSafe(templateLib, 'Template'); + + LibraryElement get applicationLib => + _resolver.getLibrary(_applicationLibAssetId); + + FunctionElement get bootstrapMethod { + if (_bootstrapMethod == null) { + _bootstrapMethod = applicationLib.definingCompilationUnit.functions + .firstWhere((FunctionElement el) => el.name == bootstrapMethodName, + orElse: () => null); + } + return _bootstrapMethod; + } + + /// Gets the type named [name] in library [lib]. Returns `null` if [lib] is + /// `null` or [name] cannot be found in [lib]. + ClassElement _getTypeSafe(LibraryElement lib, String name) { + if (lib == null) return null; + return lib.getType(name); + } + + /// Whether [clazz] is annotated as a [Component]. + bool isComponent(ClassElement clazz) => + hasAnnotation(clazz, componentAnnotation); +} + +/// Whether [type], its superclass, or one of its interfaces matches [target]. +bool isAnnotationMatch(InterfaceType type, ClassElement target) { + if (type == null || type.element == null) return false; + if (type.element.type == target.type) return true; + if (isAnnotationMatch(type.superclass, target)) return true; + for (var interface in type.interfaces) { + if (isAnnotationMatch(interface, target)) return true; + } + return false; +} + +/// Determines whether [clazz] has at least one annotation that `is` a +/// [metaClazz]. +bool hasAnnotation(ClassElement clazz, ClassElement metaClazz) { + if (clazz == null || metaClazz == null) return false; + return clazz.metadata.firstWhere((ElementAnnotation meta) { +// TODO(kegluneq): Make this recognize non-ConstructorElement annotations. + return meta.element is ConstructorElement && + isAnnotationMatch(meta.element.returnType, metaClazz); + }, orElse: () => null) != null; +} diff --git a/modules/angular2/src/transform/transformer.dart b/modules/angular2/src/transform/transformer.dart index 10a1d49b6e..2c11f95fd2 100644 --- a/modules/angular2/src/transform/transformer.dart +++ b/modules/angular2/src/transform/transformer.dart @@ -1,12 +1,16 @@ library angular2.src.transform; import 'dart:async'; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'annotation_processor.dart'; import 'codegen.dart' as codegen; +import 'find_bootstrap.dart'; import 'html_transform.dart'; +import 'logging.dart' as log; import 'options.dart'; import 'resolvers.dart'; import 'traversal.dart'; @@ -21,25 +25,29 @@ class AngularTransformer extends Transformer { AngularTransformer(this.options) : _resolvers = createResolvers(); + static const _bootstrapEntryPointParam = 'bootstrap_entry_point'; static const _entryPointParam = 'entry_point'; static const _newEntryPointParam = 'new_entry_point'; static const _htmlEntryPointParam = 'html_entry_point'; factory AngularTransformer.asPlugin(BarbackSettings settings) { + var bootstrapEntryPoint = settings.configuration[_bootstrapEntryPointParam]; var entryPoint = settings.configuration[_entryPointParam]; var newEntryPoint = settings.configuration[_newEntryPointParam]; if (newEntryPoint == null) { newEntryPoint = entryPoint.replaceFirst('.dart', '.bootstrap.dart'); } var htmlEntryPoint = settings.configuration[_htmlEntryPointParam]; - return new AngularTransformer( - new TransformerOptions(entryPoint, newEntryPoint, htmlEntryPoint)); + return new AngularTransformer(new TransformerOptions( + bootstrapEntryPoint, entryPoint, newEntryPoint, htmlEntryPoint)); } bool isPrimary(AssetId id) => options.entryPoint == id.path || options.htmlEntryPoint == id.path; Future apply(Transform transform) { + log.init(transform); + if (transform.primaryInput.id.path == options.entryPoint) { return _buildBootstrapFile(transform); } else if (transform.primaryInput.id.path == options.htmlEntryPoint) { @@ -49,17 +57,24 @@ class AngularTransformer extends Transformer { } Future _buildBootstrapFile(Transform transform) { + var bootstrapEntryPointId = new AssetId( + transform.primaryInput.id.package, options.bootstrapEntryPoint); var newEntryPointId = new AssetId(transform.primaryInput.id.package, options.newEntryPoint); return transform.hasInput(newEntryPointId).then((exists) { if (exists) { - transform.logger + log.logger .error('New entry point file $newEntryPointId already exists.'); } else { return _resolvers.get(transform).then((resolver) { try { new _BootstrapFileBuilder(resolver, transform, - transform.primaryInput.id, newEntryPointId).run(); + transform.primaryInput.id, bootstrapEntryPointId, + newEntryPointId).run(); + } catch (err, stackTrace) { + log.logger.error('${err}: ${stackTrace}', + asset: bootstrapEntryPointId); + rethrow; } finally { resolver.release(); } @@ -72,28 +87,40 @@ class AngularTransformer extends Transformer { class _BootstrapFileBuilder { final Resolver _resolver; final Transform _transform; + final AssetId _bootstrapEntryPoint; final AssetId _entryPoint; final AssetId _newEntryPoint; - AnnotationMatcher _directiveInfo; - _BootstrapFileBuilder(Resolver resolver, Transform transform, - this._entryPoint, this._newEntryPoint) + this._entryPoint, this._bootstrapEntryPoint, this._newEntryPoint) : _resolver = resolver, - _transform = transform, - _directiveInfo = new AnnotationMatcher(resolver - .getLibrary(new AssetId( - 'angular2', 'lib/src/core/annotations/annotations.dart')) - .getType('Directive')); + _transform = transform; /// Adds the new entry point file to the transform. Should only be ran once. void run() { var entryLib = _resolver.getLibrary(_entryPoint); - new ImportTraversal(_directiveInfo).traverse(entryLib); + Set bootstrapCalls = findBootstrapCalls( + _resolver, _resolver.getLibrary(_bootstrapEntryPoint)); + + log.logger.info('found ${bootstrapCalls.length} call(s) to `bootstrap`'); + bootstrapCalls.forEach((BootstrapCallInfo info) { + log.logger.info('Arg1: ${info.bootstrapType}'); + }); + + var types = new Angular2Types(_resolver); + // TODO(kegluneq): Also match [Inject]. + var matcher = new AnnotationMatcher(new Set.from([ + types.componentAnnotation, + types.decoratorAnnotation, + types.templateAnnotation + ])); + + var traversal = new AngularVisibleTraversal(types, matcher); + bootstrapCalls.forEach((call) => traversal.traverse(call.bootstrapType)); var context = new codegen.Context(logger: _transform.logger); - _directiveInfo.matchQueue + matcher.matchQueue .forEach((entry) => context.directiveRegistry.register(entry)); _transform.addOutput(new Asset.fromString(_newEntryPoint, codegen diff --git a/modules/angular2/src/transform/traversal.dart b/modules/angular2/src/transform/traversal.dart index e762ae1ff5..1bbc1a890d 100644 --- a/modules/angular2/src/transform/traversal.dart +++ b/modules/angular2/src/transform/traversal.dart @@ -1,102 +1,83 @@ library angular2.src.transform; +import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; -import 'package:path/path.dart' as path; import 'annotation_processor.dart'; +import 'logging.dart'; +import 'resolvers.dart'; -class ImportTraversal { - final AnnotationMatcher _annotationMatcher; +/// Walks through an Angular2 application, finding all classes matching the +/// provided [annotationMatcher]. +class AngularVisibleTraversal { + final Angular2Types _types; + final _ComponentParsingAstVisitor _visitor; - ImportTraversal(this._annotationMatcher); + AngularVisibleTraversal(this._types, AnnotationMatcher annotationMatcher) + : _visitor = new _ComponentParsingAstVisitor(annotationMatcher); - /// Reads Initializer annotations on this library and all its dependencies in - /// post-order. - void traverse(LibraryElement library, [Set seen]) { - if (seen == null) seen = new Set(); - seen.add(library); - - // Visit all our dependencies. - for (var importedLibrary in _sortedLibraryImports(library)) { - // Don't include anything from the sdk. - if (importedLibrary.isInSdk) continue; - if (seen.contains(importedLibrary)) continue; - traverse(importedLibrary, seen); - } - - for (var clazz in _classesOfLibrary(library, seen)) { - var superClass = clazz.supertype; - while (superClass != null) { - if (_annotationMatcher.processAnnotations(superClass.element) && - superClass.element.library != clazz.library) { - _logger.warning( - 'We have detected a cycle in your import graph when running ' - 'initializers on ${clazz.name}. This means the super class ' - '${superClass.name} has a dependency on this library ' - '(possibly transitive).'); - } - superClass = superClass.superclass; - } - _annotationMatcher.processAnnotations(clazz); + /// Walks an Angular2 application, starting with the class represented by + /// [entryPoint], which must be annotated as an Angular2 [Component]. + /// + /// We recursively check the entryPoint's annotations and constructor + /// arguments for types which match the provided [annotationMatcher]. + void traverse(ClassElement entryPoint) { + if (!_types.isComponent(entryPoint)) { + throw new ArgumentError.value(entryPoint, 'entryPoint', + 'Provided entryPoint must be annotated as a Component'); } + entryPoint.node.accept(_visitor); + } +} + +class _ComponentParsingAstVisitor extends Object + with RecursiveAstVisitor { + final Set _seen = new Set(); + final AnnotationMatcher _matcher; + + _ComponentParsingAstVisitor(this._matcher); + + @override + Object visitClassDeclaration(ClassDeclaration node) { + if (node.element != null) { + if (_seen.contains(node.element)) return null; + _seen.add(node.element); + } + + // Process the class itself. + node.name.accept(this); + + // Process metadata information, ignoring [FieldDeclaration]s and + // [MethodDeclaration]s (see below). + node.metadata.forEach((Annotation meta) => meta.accept(this)); + + // Process constructor parameters, fields & methods are ignored below. + node.members.forEach((m) => m.accept(this)); + + return null; + } + + @override + Object visitFieldDeclaration(FieldDeclaration node) => null; + + @override + Object visitMethodDeclaration(MethodDeclaration node) => null; + + @override + Object visitAnnotation(Annotation node) { + // TODO(kegluneq): Visit only Angular2 annotations & subtypes. + return super.visitAnnotation(node); + } + + @override + Object visitSimpleIdentifier(SimpleIdentifier node) { + if (node.bestElement != null) { + if (node.bestElement is ClassElement) { + var matches = _matcher.processAnnotations(node.bestElement); + // If any of these types are matches, recurse on them. + matches.forEach((match) => match.node.accept(this)); + } + } + return super.visitSimpleIdentifier(node); } - - /// Retrieves all classes that are visible if you were to import [lib]. This - /// includes exported classes from other libraries. - List _classesOfLibrary( - LibraryElement library, Set seen) { - var result = []; - result.addAll(library.units.expand((u) => u.types)); - for (var export in library.exports) { - if (seen.contains(export.exportedLibrary)) continue; - var exported = _classesOfLibrary(export.exportedLibrary, seen); - _filter(exported, export.combinators); - result.addAll(exported); - } - result.sort((a, b) => a.name.compareTo(b.name)); - return result; - } - - /// Filters [elements] that come from an export, according to its show/hide - /// combinators. This modifies [elements] in place. - void _filter(List elements, List combinators) { - for (var c in combinators) { - if (c is ShowElementCombinator) { - var show = c.shownNames.toSet(); - elements.retainWhere((e) => show.contains(e.displayName)); - } else if (c is HideElementCombinator) { - var hide = c.hiddenNames.toSet(); - elements.removeWhere((e) => hide.contains(e.displayName)); - } - } - } - - Iterable _sortedLibraryImports(LibraryElement library) => - (new List.from(library.imports) - ..sort((ImportElement a, ImportElement b) { - // dart: imports don't have a uri - if (a.uri == null && b.uri != null) return -1; - if (b.uri == null && a.uri != null) return 1; - if (a.uri == null && b.uri == null) { - return a.importedLibrary.name.compareTo(b.importedLibrary.name); - } - - // package: imports next - var aIsPackage = a.uri.startsWith('package:'); - var bIsPackage = b.uri.startsWith('package:'); - if (aIsPackage && !bIsPackage) { - return -1; - } else if (bIsPackage && !aIsPackage) { - return 1; - } else if (bIsPackage && aIsPackage) { - return a.uri.compareTo(b.uri); - } - - // And finally compare based on the relative uri if both are file paths. - var aUri = path.relative(a.source.uri.path, - from: path.dirname(library.source.uri.path)); - var bUri = path.relative(b.source.uri.path, - from: path.dirname(library.source.uri.path)); - return aUri.compareTo(bUri); - })).map((import) => import.importedLibrary); } diff --git a/modules/angular2/test/transform/common.dart b/modules/angular2/test/transform/common.dart new file mode 100644 index 0000000000..e2fce446ea --- /dev/null +++ b/modules/angular2/test/transform/common.dart @@ -0,0 +1,14 @@ +// 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. +library angular2.test.transform; + +import 'dart:async'; + +/// Mocked out version of [bootstrap], defined in application.dart. Importing +/// the actual file in tests causes issues with resolution due to its +/// transitive dependencies. +Future bootstrap(Type appComponentType, [List bindings = null, + Function givenBootstrapErrorReporter = null]) { + return null; +} diff --git a/modules/angular2/test/transform/html_entry_point_files/index.dart b/modules/angular2/test/transform/html_entry_point_files/index.dart index 066068bf76..83ee134e2e 100644 --- a/modules/angular2/test/transform/html_entry_point_files/index.dart +++ b/modules/angular2/test/transform/html_entry_point_files/index.dart @@ -1,5 +1,6 @@ library web_foo; import 'package:angular2/src/core/annotations/annotations.dart'; +import 'package:angular2/src/core/application.dart'; void main() {} diff --git a/modules/angular2/test/transform/list_of_types_files/index.dart b/modules/angular2/test/transform/list_of_types_files/index.dart index f65483f03e..0af996b536 100644 --- a/modules/angular2/test/transform/list_of_types_files/index.dart +++ b/modules/angular2/test/transform/list_of_types_files/index.dart @@ -1,7 +1,8 @@ library web_foo; +import 'package:angular2/src/core/application.dart'; import 'bar.dart'; void main() { - new Component('Things'); + bootstrap(MyComponent); } diff --git a/modules/angular2/test/transform/simple_annotation_files/index.dart b/modules/angular2/test/transform/simple_annotation_files/index.dart index ca52b6d537..0af996b536 100644 --- a/modules/angular2/test/transform/simple_annotation_files/index.dart +++ b/modules/angular2/test/transform/simple_annotation_files/index.dart @@ -1,7 +1,8 @@ library web_foo; +import 'package:angular2/src/core/application.dart'; import 'bar.dart'; void main() { - new MyComponent('Things'); + bootstrap(MyComponent); } diff --git a/modules/angular2/test/transform/synthetic_ctor_files/index.dart b/modules/angular2/test/transform/synthetic_ctor_files/index.dart index 73b7d455d0..0af996b536 100644 --- a/modules/angular2/test/transform/synthetic_ctor_files/index.dart +++ b/modules/angular2/test/transform/synthetic_ctor_files/index.dart @@ -1,7 +1,8 @@ library web_foo; +import 'package:angular2/src/core/application.dart'; import 'bar.dart'; void main() { - new MyComponent(); + bootstrap(MyComponent); } diff --git a/modules/angular2/test/transform/transform_test.dart b/modules/angular2/test/transform/transform_test.dart index 32c3988212..ebaaa342ab 100644 --- a/modules/angular2/test/transform/transform_test.dart +++ b/modules/angular2/test/transform/transform_test.dart @@ -17,7 +17,7 @@ main() { } var formatter = new DartFormatter(); -var transform = new AngularTransformer(new TransformerOptions( +var transform = new AngularTransformer(new TransformerOptions('web/index.dart', 'web/index.dart', 'web/index.bootstrap.dart', 'web/index.html')); class TestConfig { @@ -40,7 +40,8 @@ void _runTests() { 'a|web/index.html': 'common.html', 'a|web/index.dart': 'html_entry_point_files/index.dart', 'angular2|lib/src/core/annotations/annotations.dart': - '../../lib/src/core/annotations/annotations.dart' + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' }, outputs: { 'a|web/index.html': 'html_entry_point_files/expected/index.html' @@ -51,7 +52,8 @@ void _runTests() { 'a|web/index.dart': 'simple_annotation_files/index.dart', 'a|web/bar.dart': 'simple_annotation_files/bar.dart', 'angular2|lib/src/core/annotations/annotations.dart': - '../../lib/src/core/annotations/annotations.dart' + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' }, outputs: { 'a|web/index.bootstrap.dart': @@ -64,7 +66,8 @@ void _runTests() { 'a|web/foo.dart': 'two_deps_files/foo.dart', 'a|web/bar.dart': 'two_deps_files/bar.dart', 'angular2|lib/src/core/annotations/annotations.dart': - '../../lib/src/core/annotations/annotations.dart' + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' }, outputs: { 'a|web/index.bootstrap.dart': @@ -77,7 +80,8 @@ void _runTests() { 'a|web/foo.dart': 'list_of_types_files/foo.dart', 'a|web/bar.dart': 'list_of_types_files/bar.dart', 'angular2|lib/src/core/annotations/annotations.dart': - '../../lib/src/core/annotations/annotations.dart' + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' }, outputs: { 'a|web/index.bootstrap.dart': @@ -89,12 +93,28 @@ void _runTests() { 'a|web/index.dart': 'synthetic_ctor_files/index.dart', 'a|web/bar.dart': 'synthetic_ctor_files/bar.dart', 'angular2|lib/src/core/annotations/annotations.dart': - '../../lib/src/core/annotations/annotations.dart' + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' }, outputs: { 'a|web/index.bootstrap.dart': 'synthetic_ctor_files/expected/index.bootstrap.dart' - }) + }), + new TestConfig('Component with two annotations', + inputs: { + 'a|web/index.html': 'common.html', + 'a|web/index.dart': 'two_annotations_files/index.dart', + 'a|web/bar.dart': 'two_annotations_files/bar.dart', + 'angular2|lib/src/core/annotations/annotations.dart': + '../../lib/src/core/annotations/annotations.dart', + 'angular2|lib/src/core/annotations/template.dart': + '../../lib/src/core/annotations/template.dart', + 'angular2|lib/src/core/application.dart': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'two_annotations_files/expected/index.bootstrap.dart' + }), ]; var cache = {}; diff --git a/modules/angular2/test/transform/two_annotations_files/bar.dart b/modules/angular2/test/transform/two_annotations_files/bar.dart new file mode 100644 index 0000000000..880e8c5b28 --- /dev/null +++ b/modules/angular2/test/transform/two_annotations_files/bar.dart @@ -0,0 +1,10 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'package:angular2/src/core/annotations/template.dart'; + +@Component(selector: '[soup]') +@Template(inline: 'Salad') +class MyComponent { + MyComponent(); +} diff --git a/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart new file mode 100644 index 0000000000..5ca6dde3a5 --- /dev/null +++ b/modules/angular2/test/transform/two_annotations_files/expected/index.bootstrap.dart @@ -0,0 +1,18 @@ +import 'package:angular2/src/reflection/reflection.dart' show reflector; +import 'bar.dart' as i0; +import 'package:angular2/src/core/annotations/annotations.dart' as i1; +import 'package:angular2/src/core/annotations/template.dart' as i2; +import 'index.dart' as i3; + +main() { + reflector + ..registerType(i0.MyComponent, { + "factory": () => new i0.MyComponent(), + "parameters": const [const []], + "annotations": const [ + const i1.Component(selector: '[soup]'), + const i2.Template(inline: 'Salad') + ] + }); + i3.main(); +} diff --git a/modules/angular2/test/transform/two_annotations_files/index.dart b/modules/angular2/test/transform/two_annotations_files/index.dart new file mode 100644 index 0000000000..0af996b536 --- /dev/null +++ b/modules/angular2/test/transform/two_annotations_files/index.dart @@ -0,0 +1,8 @@ +library web_foo; + +import 'package:angular2/src/core/application.dart'; +import 'bar.dart'; + +void main() { + bootstrap(MyComponent); +} diff --git a/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart index 5676e38ced..484f5bf3f3 100644 --- a/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart +++ b/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart @@ -10,9 +10,7 @@ main() { "factory": (i1.MyContext c, String inValue) => new i0.MyComponent(c, inValue), "parameters": const [const [i1.MyContext, String]], - "annotations": const [ - const i2.Component(selector: i1.preDefinedSelector) - ] + "annotations": const [const i2.Component(selector: i1.preDefinedSelector)] }); i3.main(); } diff --git a/modules/angular2/test/transform/two_deps_files/index.dart b/modules/angular2/test/transform/two_deps_files/index.dart index ca52b6d537..0af996b536 100644 --- a/modules/angular2/test/transform/two_deps_files/index.dart +++ b/modules/angular2/test/transform/two_deps_files/index.dart @@ -1,7 +1,8 @@ library web_foo; +import 'package:angular2/src/core/application.dart'; import 'bar.dart'; void main() { - new MyComponent('Things'); + bootstrap(MyComponent); } diff --git a/modules/examples/pubspec.yaml b/modules/examples/pubspec.yaml index 5bc3fd2ebd..dfde3a67f8 100644 --- a/modules/examples/pubspec.yaml +++ b/modules/examples/pubspec.yaml @@ -9,6 +9,7 @@ dev_dependencies: guinness: ">=0.1.16 <0.2.0" transformers: - angular2: + bootstrap_entry_point: web/src/hello_world/index_common.dart entry_point: web/src/hello_world/index.dart html_entry_point: web/src/hello_world/index.html - $dart2js: