diff --git a/gulpfile.js b/gulpfile.js index ffa20a4eec..2b4fde23e6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -516,11 +516,17 @@ gulp.task('ci', function(done) { ); }); +gulp.task('tests/transform.dart', function() { + return gulp.src('modules/angular2/test/transform/**') + .pipe(gulp.dest('dist/dart/angular2/test/transform')); +}); + // ----------------- // orchestrated targets gulp.task('build.dart', function(done) { runSequence( ['build/deps.js.dart2js', 'build/transpile.dart', 'build/html.dart'], + 'tests/transform.dart', 'build/pubspec.dart', 'build/multicopy.dart', 'build/pubbuild.dart', diff --git a/modules/angular2/pubspec.yaml b/modules/angular2/pubspec.yaml index 3bfaf62c68..09a5881867 100644 --- a/modules/angular2/pubspec.yaml +++ b/modules/angular2/pubspec.yaml @@ -9,6 +9,11 @@ homepage: <%= packageJson.homepage %> environment: sdk: '>=1.4.0' dependencies: + analyzer: '0.22.4' + barback: '0.15.2+2' + code_transformers: '0.2.5' + dart_style: '0.1.3' + html5lib: '0.12.0' stack_trace: '>=1.1.1 <1.2.0' dev_dependencies: guinness: ">=0.1.16 <0.2.0" diff --git a/modules/angular2/src/transform/annotation_processor.dart b/modules/angular2/src/transform/annotation_processor.dart new file mode 100644 index 0000000000..323aa23df8 --- /dev/null +++ b/modules/angular2/src/transform/annotation_processor.dart @@ -0,0 +1,58 @@ +import 'dart:collection' show Queue; +import 'package:analyzer/src/generated/element.dart'; + +/// Provides a mechanism for checking an element for the provided +/// [_annotationClass] and reporting the resulting (element, annotation) pairs. +class AnnotationMatcher { + /// Queue for annotations. + final initQueue = new Queue(); + /// 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; + + AnnotationMatcher(this._annotationClass); + + /// 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 been seen previously. + bool processAnnotations(ClassElement element) { + var found = false; + 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); + }).where((ElementAnnotation meta) { + // Only process ([element], [meta]) combinations we haven't seen previously. + return !_seenAnnotations + .putIfAbsent(element, () => new Set()) + .contains(meta); + }).forEach((ElementAnnotation meta) { + _seenAnnotations[element].add(meta); + initQueue.addLast(new AnnotationMatch(element, meta)); + found = true; + }); + return found; + } + + /// 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; + } +} + +// Element/ElementAnnotation pair. +class AnnotationMatch { + final Element element; + final ElementAnnotation annotation; + + AnnotationMatch(this.element, this.annotation); +} diff --git a/modules/angular2/src/transform/codegen.dart b/modules/angular2/src/transform/codegen.dart new file mode 100644 index 0000000000..399cc27701 --- /dev/null +++ b/modules/angular2/src/transform/codegen.dart @@ -0,0 +1,390 @@ +library angular2.transformer; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/java_core.dart'; +import 'package:barback/barback.dart' show AssetId, TransformLogger; +import 'package:dart_style/dart_style.dart'; +import 'package:path/path.dart' as path; + +import 'annotation_processor.dart'; + +/// Base class that maintains codegen state. +class Context { + final TransformLogger _logger; + /// Maps libraries to the import prefixes we will use in the newly + /// generated code. + final Map _libraryPrefixes; + + DirectiveRegistry _directiveRegistry; + DirectiveRegistry get directiveRegistry => _directiveRegistry; + + Context({TransformLogger logger}) + : _logger = logger, + _libraryPrefixes = {} { + _directiveRegistry = new _DirectiveRegistryImpl(this); + } + + void error(String errorString) { + if (_logger != null) { + _logger.error(errorString); + } else { + throw new Error(errorString); + } + } + + /// If elements in [lib] should be prefixed in our generated code, returns + /// the appropriate prefix followed by a `.`. Future items from the same + /// library will use the same prefix. + /// If [lib] does not need a prefix, returns the empty string. + String _getPrefixDot(LibraryElement lib) { + var prefix = lib != null && !lib.isInSdk + ? _libraryPrefixes.putIfAbsent(lib, () => 'i${_libraryPrefixes.length}') + : null; + return prefix == null ? '' : '${prefix}.'; + } +} + +abstract class DirectiveRegistry { + // Adds [entry] to the `registerType` calls which will be generated. + void register(AnnotationMatch entry); +} + +const _reflectorImport = + 'import \'package:angular2/src/reflection/reflection.dart\' ' + 'show reflector;'; + +/// Default implementation to map from [LibraryElement] to [AssetId]. This +/// assumes that [el.source] has a getter called [assetId]. +AssetId _assetIdFromLibraryElement(LibraryElement el) { + return (el.source as dynamic).assetId; +} + +String codegenEntryPoint(Context context, + {LibraryElement entryPoint, AssetId newEntryPoint}) { + // This must be called prior to [codegenImports] or the entry point + // library will not be imported. + var entryPointPrefix = context._getPrefixDot(entryPoint); + // TODO(jakemac): copyright and library declaration + var outBuffer = new StringBuffer(_reflectorImport); + _codegenImports(context, newEntryPoint, outBuffer); + outBuffer + ..write('main() {') + ..write(context.directiveRegistry.toString()) + ..write('${entryPointPrefix}main();}'); + + return new DartFormatter().format(outBuffer.toString()); +} + +String _codegenImports( + Context context, AssetId newEntryPoint, StringBuffer buffer) { + context._libraryPrefixes.forEach((lib, prefix) { + buffer + ..write(_codegenImport( + context, _assetIdFromLibraryElement(lib), newEntryPoint)) + ..writeln('as ${prefix};'); + }); +} + +_codegenImport(Context context, AssetId libraryId, AssetId entryPoint) { + if (libraryId.path.startsWith('lib/')) { + var packagePath = libraryId.path.replaceFirst('lib/', ''); + return "import 'package:${libraryId.package}/${packagePath}'"; + } else if (libraryId.package != entryPoint.package) { + context._error("Can't import `${libraryId}` from `${entryPoint}`"); + } else if (path.url.split(libraryId.path)[0] == + path.url.split(entryPoint.path)[0]) { + var relativePath = + path.relative(libraryId.path, from: path.dirname(entryPoint.path)); + return "import '${relativePath}'"; + } else { + context._error("Can't import `${libraryId}` from `${entryPoint}`"); + } +} + +class _DirectiveRegistryImpl implements DirectiveRegistry { + final Context _context; + final StringBuffer _buffer = new StringBuffer(); + + _DirectiveRegistryImpl(this._context); + + @override + String toString() { + return _buffer.isEmpty ? '' : 'reflector${_buffer};'; + } + + // Adds [entry] to the `registerType` calls which will be generated. + void register(AnnotationMatch entry) { + var element = entry.element; + var annotation = entry.annotation; + + if (annotation.element is! ConstructorElement) { + _context._error('Unsupported annotation type. ' + 'Only constructors are supported as Directives.'); + return; + } + if (element is! ClassElement) { + _context._error('Directives can only be applied to classes.'); + return; + } + if (element.node is! ClassDeclaration) { + _context._error('Unsupported annotation type. ' + 'Only class declarations are supported as Directives.'); + return; + } + final ConstructorElement ctor = element.unnamedConstructor; + if (ctor == null) { + _context._error('No default constructor found for ${element.name}'); + return; + } + + _buffer.writeln('..registerType(${_codegenClassTypeString(element)}, {' + '"factory": ${_codegenFactoryProp(ctor)},' + '"parameters": ${_codegenParametersProp(ctor)},' + '"annotations": ${_codegenAnnotationsProp(element)}' + '})'); + } + + String _codegenClassTypeString(ClassElement el) { + return '${_context._getPrefixDot(el.library)}${el.name}'; + } + + /// Creates the 'annotations' property for the Angular2 [registerType] call + /// for [el]. + String _codegenAnnotationsProp(ClassElement el) { + var writer = new PrintStringWriter(); + var visitor = new _AnnotationsTransformVisitor(writer, _context); + el.node.accept(visitor); + return writer.toString(); + } + + /// Creates the 'factory' property for the Angular2 [registerType] call + /// for [ctor]. + String _codegenFactoryProp(ConstructorElement ctor) { + if (ctor.node == null) { + // This occurs when the class does not declare a constructor. + var prefix = _context._getPrefixDot(ctor.type.element.library); + return '() => new ${prefix}${ctor.enclosingElement.displayName}()'; + } else { + var writer = new PrintStringWriter(); + var visitor = new _FactoryTransformVisitor(writer, _context); + ctor.node.accept(visitor); + return writer.toString(); + } + } + + /// Creates the 'parameters' property for the Angular2 [registerType] call + /// for [ctor]. + String _codegenParametersProp(ConstructorElement ctor) { + if (ctor.node == null) { + // This occurs when the class does not declare a constructor. + return 'const [const []]'; + } else { + var writer = new PrintStringWriter(); + var visitor = new _ParameterTransformVisitor(writer, _context); + ctor.node.accept(visitor); + return writer.toString(); + } + } +} + +/// Visitor providing common methods for concrete implementations. +abstract class _TransformVisitor extends ToSourceVisitor { + final Context _context; + final PrintWriter _writer; + + _TransformVisitor(PrintWriter writer, this._context) + : this._writer = writer, + super(writer); + + /// Safely visit the given node. + /// @param node the node to be visited + void _visitNode(AstNode node) { + if (node != null) { + node.accept(this); + } + } + + /** + * Safely visit the given node, printing the prefix before the node if it is non-`null`. + * + * @param prefix the prefix to be printed if there is a node to visit + * @param node the node to be visited + */ + void _visitNodeWithPrefix(String prefix, AstNode node) { + if (node != null) { + _writer.print(prefix); + node.accept(this); + } + } + + /** + * Safely visit the given node, printing the suffix after the node if it is non-`null`. + * + * @param suffix the suffix to be printed if there is a node to visit + * @param node the node to be visited + */ + void _visitNodeWithSuffix(AstNode node, String suffix) { + if (node != null) { + node.accept(this); + _writer.print(suffix); + } + } + + @override + Object visitSimpleIdentifier(SimpleIdentifier node) { + // Make sure the identifier is prefixed if necessary. + if (node.bestElement is ClassElementImpl || + node.bestElement is PropertyAccessorElement) { + _writer + ..print(_context._getPrefixDot(node.bestElement.library)) + ..print(node.token.lexeme); + } else { + return super.visitSimpleIdentifier(node); + } + return null; + } +} + +/// SourceVisitor designed to accept [ConstructorDeclaration] nodes. +class _CtorTransformVisitor extends _TransformVisitor { + bool _withParameterTypes = true; + bool _withParameterNames = true; + + _CtorTransformVisitor(PrintWriter writer, Context _context) + : super(writer, _context); + + /// If [_withParameterTypes] is true, this method outputs [node]'s type + /// (appropriately prefixed based on [_libraryPrefixes]. If + /// [_withParameterNames] is true, this method outputs [node]'s identifier. + Object _visitNormalFormalParameter(NormalFormalParameter node) { + if (_withParameterTypes) { + var paramType = node.element.type; + var prefix = _context._getPrefixDot(paramType.element.library); + _writer.print('${prefix}${paramType.displayName}'); + if (_withParameterNames) { + _visitNodeWithPrefix(' ', node.identifier); + } + } else if (_withParameterNames) { + _visitNode(node.identifier); + } + return null; + } + + @override + Object visitSimpleFormalParameter(SimpleFormalParameter node) { + return _visitNormalFormalParameter(node); + } + + @override + Object visitFieldFormalParameter(FieldFormalParameter node) { + if (node.parameters != null) { + _context.error('Parameters in ctor not supported ' + '(${super.visitFormalParameterList(node)}'); + } + return _visitNormalFormalParameter(node); + } + + @override + Object visitDefaultFormalParameter(DefaultFormalParameter node) { + _visitNode(node.parameter); + return null; + } + + @override + /// Overridden to avoid outputting grouping operators for default parameters. + Object visitFormalParameterList(FormalParameterList node) { + _writer.print('('); + NodeList parameters = node.parameters; + int size = parameters.length; + for (int i = 0; i < size; i++) { + if (i > 0) { + _writer.print(', '); + } + parameters[i].accept(this); + } + _writer.print(')'); + return null; + } +} + +/// ToSourceVisitor designed to print 'parameters' values for Angular2's +/// [registerType] calls. +class _ParameterTransformVisitor extends _CtorTransformVisitor { + _ParameterTransformVisitor(PrintWriter writer, Context _context) + : super(writer, _context); + + @override + Object visitConstructorDeclaration(ConstructorDeclaration node) { + _withParameterNames = false; + _withParameterTypes = true; + _writer.print('const [const ['); + _visitNode(node.parameters); + _writer.print(']]'); + return null; + } + + @override + Object visitFormalParameterList(FormalParameterList node) { + NodeList parameters = node.parameters; + int size = parameters.length; + for (int i = 0; i < size; i++) { + if (i > 0) { + _writer.print(', '); + } + parameters[i].accept(this); + } + return null; + } +} + +/// ToSourceVisitor designed to print 'factory' values for Angular2's +/// [registerType] calls. +class _FactoryTransformVisitor extends _CtorTransformVisitor { + _FactoryTransformVisitor(PrintWriter writer, Context _context) + : super(writer, _context); + + @override + Object visitConstructorDeclaration(ConstructorDeclaration node) { + _withParameterNames = true; + _withParameterTypes = true; + _visitNode(node.parameters); + _writer.print(' => new '); + _visitNode(node.returnType); + _visitNodeWithPrefix(".", node.name); + _withParameterTypes = false; + _visitNode(node.parameters); + return null; + } +} + +/// ToSourceVisitor designed to print a [ClassDeclaration] node as a +/// 'annotations' value for Angular2's [registerType] calls. +class _AnnotationsTransformVisitor extends _TransformVisitor { + _AnnotationsTransformVisitor(PrintWriter writer, Context _context) + : super(writer, _context); + + @override + Object visitClassDeclaration(ClassDeclaration node) { + _writer.print('const ['); + var size = node.metadata.length; + for (var i = 0; i < size; ++i) { + if (i > 0) { + _writer.print(', '); + } + node.metadata[i].accept(this); + } + _writer.print(']'); + return null; + } + + @override + Object visitAnnotation(Annotation node) { + _writer.print('const '); + _visitNode(node.name); +// TODO(tjblasi): Do we need to handle named constructors for annotations? +// _visitNodeWithPrefix(".", node.constructorName); + _visitNode(node.arguments); + return null; + } +} diff --git a/modules/angular2/src/transform/html_transform.dart b/modules/angular2/src/transform/html_transform.dart new file mode 100644 index 0000000000..dcfbf7ee11 --- /dev/null +++ b/modules/angular2/src/transform/html_transform.dart @@ -0,0 +1,52 @@ +library angular2.transformer; + +import 'dart:async'; +import 'package:barback/barback.dart'; +import 'package:html5lib/dom.dart' as dom; +import 'package:html5lib/parser.dart' show parse; +import 'package:path/path.dart' as path; + +import 'options.dart'; + +Future transformHtmlEntryPoint( + TransformerOptions options, Transform transform) { + // For now at least, [options.htmlEntryPoint], [options.entryPoint], and + // [options.newEntryPoint] need to be in the same folder. + // TODO(jakemac): support package urls with [options.entryPoint] or + // [options.newEntryPoint] in `lib`, and [options.htmlEntryPoint] in another + // directory. + var _expectedDir = path.split(options.htmlEntryPoint)[0]; + if (!options.inSameTopLevelDir()) { + transform.logger.error( + '${options.htmlEntryPointParam}, ${options.entryPointParam}, and ' + '${options.newEntryPointParam} (if supplied) all must be in the ' + 'same top level directory.'); + } + + // The relative path from [options.htmlEntryPoint] to [dartEntry]. You must + // ensure that neither of these is null before calling this function. + String _relativeDartEntryPath(String dartEntry) => + path.relative(dartEntry, from: path.dirname(options.htmlEntryPoint)); + + // Checks if the src of this script tag is pointing at `options.entryPoint`. + bool _isEntryPointScript(dom.Element script) => + path.normalize(script.attributes['src']) == + _relativeDartEntryPath(options.entryPoint); + + return transform.primaryInput.readAsString().then((String html) { + var found = false; + var doc = parse(html); + var scripts = doc.querySelectorAll('script[type="application/dart"]'); + for (dom.Element script in scripts) { + if (!_isEntryPointScript(script)) continue; + script.attributes['src'] = _relativeDartEntryPath(options.newEntryPoint); + found = true; + } + if (!found) { + transform.logger.error('Unable to find script for ${options.entryPoint} ' + 'in ${options.htmlEntryPoint}.'); + } + return transform.addOutput( + new Asset.fromString(transform.primaryInput.id, doc.outerHtml)); + }); +} diff --git a/modules/angular2/src/transform/options.dart b/modules/angular2/src/transform/options.dart new file mode 100644 index 0000000000..eff1805679 --- /dev/null +++ b/modules/angular2/src/transform/options.dart @@ -0,0 +1,17 @@ +library angular2.transformer; + +import 'package:path/path.dart' as path; + +class TransformerOptions { + final String entryPoint; + final String newEntryPoint; + final String htmlEntryPoint; + + TransformerOptions(this.entryPoint, this.newEntryPoint, this.htmlEntryPoint); + + bool inSameTopLevelDir() { + var expectedDir = path.split(htmlEntryPoint)[0]; + return (expectedDir == path.split(entryPoint)[0] && + expectedDir == path.split(newEntryPoint)[0]); + } +} diff --git a/modules/angular2/src/transform/resolvers.dart b/modules/angular2/src/transform/resolvers.dart new file mode 100644 index 0000000000..b6edb49727 --- /dev/null +++ b/modules/angular2/src/transform/resolvers.dart @@ -0,0 +1,43 @@ +import 'package:code_transformers/resolver.dart'; + +Resolvers createResolvers() { + return new Resolvers.fromMock({ + // The list of types below is derived from: + // * types that are used internally by the resolver (see + // _initializeFrom in resolver.dart). + // TODO(jakemac): Move this into code_transformers so it can be shared. + 'dart:core': ''' + library dart.core; + class Object {} + class Function {} + class StackTrace {} + class Symbol {} + class Type {} + + class String extends Object {} + class bool extends Object {} + class num extends Object {} + class int extends num {} + class double extends num {} + class DateTime extends Object {} + class Null extends Object {} + + class Deprecated extends Object { + final String expires; + const Deprecated(this.expires); + } + const Object deprecated = const Deprecated("next release"); + class _Override { const _Override(); } + const Object override = const _Override(); + class _Proxy { const _Proxy(); } + const Object proxy = const _Proxy(); + + class List extends Object {} + class Map extends Object {} + ''', + 'dart:html': ''' + library dart.html; + class HtmlElement {} + ''', + }); +} diff --git a/modules/angular2/src/transform/transformer.dart b/modules/angular2/src/transform/transformer.dart new file mode 100644 index 0000000000..9199b08f81 --- /dev/null +++ b/modules/angular2/src/transform/transformer.dart @@ -0,0 +1,103 @@ +// 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. +library angular2.transformer; + +import 'dart:async'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; + +import 'annotation_processor.dart'; +import 'codegen.dart' as codegen; +import 'html_transform.dart'; +import 'options.dart'; +import 'resolvers.dart'; +import 'traversal.dart'; + +export 'options.dart'; + +/// Removes the mirror-based initialization logic and replaces it with static +/// logic. +class AngularTransformer extends Transformer { + final Resolvers _resolvers; + final TransformerOptions options; + + AngularTransformer(this.options) : _resolvers = createResolvers(); + + static const _entryPointParam = 'entry_point'; + static const _newEntryPointParam = 'new_entry_point'; + static const _htmlEntryPointParam = 'html_entry_point'; + + factory AngularTransformer.asPlugin(BarbackSettings settings) { + 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)); + } + + bool isPrimary(AssetId id) => + options.entryPoint == id.path || options.htmlEntryPoint == id.path; + + Future apply(Transform transform) { + if (transform.primaryInput.id.path == options.entryPoint) { + return _buildBootstrapFile(transform); + } else if (transform.primaryInput.id.path == options.htmlEntryPoint) { + return transformHtmlEntryPoint(options, transform); + } + return null; + } + + Future _buildBootstrapFile(Transform transform) { + var newEntryPointId = + new AssetId(transform.primaryInput.id.package, options.newEntryPoint); + return transform.hasInput(newEntryPointId).then((exists) { + if (exists) { + transform.logger + .error('New entry point file $newEntryPointId already exists.'); + } else { + return _resolvers.get(transform).then((resolver) { + new _BootstrapFileBuilder(resolver, transform, + transform.primaryInput.id, newEntryPointId).run(); + resolver.release(); + }); + } + }); + } +} + +class _BootstrapFileBuilder { + final Resolver _resolver; + final Transform _transform; + final AssetId _entryPoint; + final AssetId _newEntryPoint; + + AnnotationMatcher _directiveInfo; + + _BootstrapFileBuilder(Resolver resolver, Transform transform, + this._entryPoint, this._newEntryPoint) + : _resolver = resolver, + _transform = transform, + _directiveInfo = new AnnotationMatcher(resolver + .getLibrary(new AssetId( + 'angular2', 'lib/src/core/annotations/annotations.dart')) + .getType('Directive')); + + /// 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); + + var context = new codegen.Context(logger: _transform.logger); + _directiveInfo.initQueue + .forEach((entry) => context.directiveRegistry.register(entry)); + + _transform.addOutput(new Asset.fromString(_newEntryPoint, codegen + .codegenEntryPoint(context, + entryPoint: entryLib, newEntryPoint: _newEntryPoint))); + } +} diff --git a/modules/angular2/src/transform/traversal.dart b/modules/angular2/src/transform/traversal.dart new file mode 100644 index 0000000000..f727b16770 --- /dev/null +++ b/modules/angular2/src/transform/traversal.dart @@ -0,0 +1,100 @@ +import 'package:analyzer/src/generated/element.dart'; +import 'package:path/path.dart' as path; + +import 'annotation_processor.dart'; + +class ImportTraversal { + final AnnotationMatcher _annotationMatcher; + + ImportTraversal(this._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); + } + } + + /// 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..77e3f98d53 --- /dev/null +++ b/modules/angular2/test/transform/common.dart @@ -0,0 +1,11 @@ +// 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 initialize.test.build.common; + +// TODO(kegluneq): Remove this and use the actual Directive def'n. +// Simple mock of Directive. +class Directive { + final context; + const Directive({this.context}); +} diff --git a/modules/angular2/test/transform/common.html b/modules/angular2/test/transform/common.html new file mode 100644 index 0000000000..739d68575a --- /dev/null +++ b/modules/angular2/test/transform/common.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/angular2/test/transform/ctor_with_default_value_files/bar.dart b/modules/angular2/test/transform/ctor_with_default_value_files/bar.dart new file mode 100644 index 0000000000..148a76db00 --- /dev/null +++ b/modules/angular2/test/transform/ctor_with_default_value_files/bar.dart @@ -0,0 +1,9 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; + +@Directive(context: 'soup') +class Component { + final dynamic c; + Component([this.c = 'sandwich']); +} diff --git a/modules/angular2/test/transform/ctor_with_default_value_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/ctor_with_default_value_files/expected/index.bootstrap.dart new file mode 100644 index 0000000000..8351124b03 --- /dev/null +++ b/modules/angular2/test/transform/ctor_with_default_value_files/expected/index.bootstrap.dart @@ -0,0 +1,14 @@ +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 'index.dart' as i2; + +main() { + reflector + ..registerType(i0.Component, { + "factory": (dynamic c) => new i0.Component(c), + "parameters": const [const [dynamic]], + "annotations": const [const i1.Directive(context: 'soup')] + }); + i2.main(); +} diff --git a/modules/angular2/test/transform/ctor_with_default_value_files/index.dart b/modules/angular2/test/transform/ctor_with_default_value_files/index.dart new file mode 100644 index 0000000000..f65483f03e --- /dev/null +++ b/modules/angular2/test/transform/ctor_with_default_value_files/index.dart @@ -0,0 +1,7 @@ +library web_foo; + +import 'bar.dart'; + +void main() { + new Component('Things'); +} diff --git a/modules/angular2/test/transform/html_entry_point_files/expected/index.html b/modules/angular2/test/transform/html_entry_point_files/expected/index.html new file mode 100644 index 0000000000..e3ded0f560 --- /dev/null +++ b/modules/angular2/test/transform/html_entry_point_files/expected/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/angular2/test/transform/html_entry_point_files/index.dart b/modules/angular2/test/transform/html_entry_point_files/index.dart new file mode 100644 index 0000000000..066068bf76 --- /dev/null +++ b/modules/angular2/test/transform/html_entry_point_files/index.dart @@ -0,0 +1,5 @@ +library web_foo; + +import 'package:angular2/src/core/annotations/annotations.dart'; + +void main() {} diff --git a/modules/angular2/test/transform/list_of_types_files/bar.dart b/modules/angular2/test/transform/list_of_types_files/bar.dart new file mode 100644 index 0000000000..a650901dab --- /dev/null +++ b/modules/angular2/test/transform/list_of_types_files/bar.dart @@ -0,0 +1,10 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart'; + +@Directive(context: const [MyContext]) +class Component { + final MyContext c; + Component(this.c); +} diff --git a/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart new file mode 100644 index 0000000000..df78c957ab --- /dev/null +++ b/modules/angular2/test/transform/list_of_types_files/expected/index.bootstrap.dart @@ -0,0 +1,15 @@ +import 'package:angular2/src/reflection/reflection.dart' show reflector; +import 'bar.dart' as i0; +import 'foo.dart' as i1; +import 'package:angular2/src/core/annotations/annotations.dart' as i2; +import 'index.dart' as i3; + +main() { + reflector + ..registerType(i0.Component, { + "factory": (i1.MyContext c) => new i0.Component(c), + "parameters": const [const [i1.MyContext]], + "annotations": const [const i2.Directive(context: const [i1.MyContext])] + }); + i3.main(); +} diff --git a/modules/angular2/test/transform/list_of_types_files/foo.dart b/modules/angular2/test/transform/list_of_types_files/foo.dart new file mode 100644 index 0000000000..5579612e44 --- /dev/null +++ b/modules/angular2/test/transform/list_of_types_files/foo.dart @@ -0,0 +1,6 @@ +library foo; + +class MyContext { + final String s; + const MyContext(this.s); +} diff --git a/modules/angular2/test/transform/list_of_types_files/index.dart b/modules/angular2/test/transform/list_of_types_files/index.dart new file mode 100644 index 0000000000..f65483f03e --- /dev/null +++ b/modules/angular2/test/transform/list_of_types_files/index.dart @@ -0,0 +1,7 @@ +library web_foo; + +import 'bar.dart'; + +void main() { + new Component('Things'); +} diff --git a/modules/angular2/test/transform/simple_annotation_files/bar.dart b/modules/angular2/test/transform/simple_annotation_files/bar.dart new file mode 100644 index 0000000000..bc846728d1 --- /dev/null +++ b/modules/angular2/test/transform/simple_annotation_files/bar.dart @@ -0,0 +1,8 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; + +@Directive(context: 'soup') +class Component { + Component(); +} diff --git a/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart new file mode 100644 index 0000000000..0d84b803bd --- /dev/null +++ b/modules/angular2/test/transform/simple_annotation_files/expected/index.bootstrap.dart @@ -0,0 +1,14 @@ +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 'index.dart' as i2; + +main() { + reflector + ..registerType(i0.Component, { + "factory": () => new i0.Component(), + "parameters": const [const []], + "annotations": const [const i1.Directive(context: 'soup')] + }); + i2.main(); +} diff --git a/modules/angular2/test/transform/simple_annotation_files/index.dart b/modules/angular2/test/transform/simple_annotation_files/index.dart new file mode 100644 index 0000000000..f65483f03e --- /dev/null +++ b/modules/angular2/test/transform/simple_annotation_files/index.dart @@ -0,0 +1,7 @@ +library web_foo; + +import 'bar.dart'; + +void main() { + new Component('Things'); +} diff --git a/modules/angular2/test/transform/synthetic_ctor_files/bar.dart b/modules/angular2/test/transform/synthetic_ctor_files/bar.dart new file mode 100644 index 0000000000..9ebd816d75 --- /dev/null +++ b/modules/angular2/test/transform/synthetic_ctor_files/bar.dart @@ -0,0 +1,6 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; + +@Directive(context: 'soup') +class Component {} diff --git a/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart b/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart new file mode 100644 index 0000000000..0d84b803bd --- /dev/null +++ b/modules/angular2/test/transform/synthetic_ctor_files/expected/index.bootstrap.dart @@ -0,0 +1,14 @@ +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 'index.dart' as i2; + +main() { + reflector + ..registerType(i0.Component, { + "factory": () => new i0.Component(), + "parameters": const [const []], + "annotations": const [const i1.Directive(context: 'soup')] + }); + i2.main(); +} diff --git a/modules/angular2/test/transform/synthetic_ctor_files/index.dart b/modules/angular2/test/transform/synthetic_ctor_files/index.dart new file mode 100644 index 0000000000..2fa127c5d5 --- /dev/null +++ b/modules/angular2/test/transform/synthetic_ctor_files/index.dart @@ -0,0 +1,7 @@ +library web_foo; + +import 'bar.dart'; + +void main() { + new Component(); +} diff --git a/modules/angular2/test/transform/transform_test.dart b/modules/angular2/test/transform/transform_test.dart new file mode 100644 index 0000000000..5405d4f357 --- /dev/null +++ b/modules/angular2/test/transform/transform_test.dart @@ -0,0 +1,126 @@ +library angular2.test; + +import 'dart:io'; +import 'package:barback/barback.dart'; +import 'package:angular2/transformer.dart'; +import 'package:code_transformers/tests.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:unittest/unittest.dart'; +import 'package:unittest/vm_config.dart'; + +import 'common.dart'; + +main() { + useVMConfiguration(); + + // TODO(kegluneq): Add a test for generating multiple annotations. + + group('Annotation tests:', _runTests); +} + +var formatter = new DartFormatter(); +var transform = new AngularTransformer(new TransformerOptions( + 'web/index.dart', 'web/index.bootstrap.dart', 'web/index.html')); + +class TestConfig { + final String name; + final Map assetPathToInputPath; + final Map assetPathToExpectedOutputPath; + + TestConfig(this.name, + {Map inputs, Map outputs}) + : this.assetPathToInputPath = inputs, + this.assetPathToExpectedOutputPath = outputs; +} + +void _runTests() { + // Each test has its own directory for inputs & an `expected` directory for + // expected outputs. + var tests = [ + new TestConfig('Html entry point', + inputs: { + 'a|web/index.html': 'common.html', + 'a|web/index.dart': 'html_entry_point_files/index.dart', + 'angular2|lib/src/core/annotations/annotations.dart': 'common.dart' + }, + outputs: { + 'a|web/index.html': 'html_entry_point_files/expected/index.html' + }), + new TestConfig('Simple', + inputs: { + 'a|web/index.html': 'common.html', + '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': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'simple_annotation_files/expected/index.bootstrap.dart' + }), + new TestConfig('Two injected dependencies', + inputs: { + 'a|web/index.html': 'common.html', + 'a|web/index.dart': 'two_deps_files/index.dart', + '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': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'two_deps_files/expected/index.bootstrap.dart' + }), + new TestConfig('List of types', + inputs: { + 'a|web/index.html': 'common.html', + 'a|web/index.dart': 'list_of_types_files/index.dart', + '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': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'list_of_types_files/expected/index.bootstrap.dart' + }), + new TestConfig('Component ctor with default value', + inputs: { + 'a|web/index.html': 'common.html', + 'a|web/index.dart': 'ctor_with_default_value_files/index.dart', + 'a|web/bar.dart': 'ctor_with_default_value_files/bar.dart', + 'angular2|lib/src/core/annotations/annotations.dart': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'ctor_with_default_value_files/expected/index.bootstrap.dart' + }), + new TestConfig('Component with synthetic Constructor', + inputs: { + 'a|web/index.html': 'common.html', + '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': 'common.dart' + }, + outputs: { + 'a|web/index.bootstrap.dart': + 'synthetic_ctor_files/expected/index.bootstrap.dart' + }) + ]; + + var cache = {}; + + for (var config in tests) { + // Read in input & output files. + config.assetPathToInputPath.forEach((key, value) { + config.assetPathToInputPath[key] = + cache.putIfAbsent(value, () => new File(value).readAsStringSync()); + }); + config.assetPathToExpectedOutputPath.forEach((key, value) { + config.assetPathToExpectedOutputPath[key] = cache.putIfAbsent(value, () { + var code = new File(value).readAsStringSync(); + return value.endsWith('dart') ? formatter.format(code) : code; + }); + }); + testPhases(config.name, [ + [transform] + ], config.assetPathToInputPath, config.assetPathToExpectedOutputPath, []); + } +} diff --git a/modules/angular2/test/transform/two_deps_files/bar.dart b/modules/angular2/test/transform/two_deps_files/bar.dart new file mode 100644 index 0000000000..1604b388a0 --- /dev/null +++ b/modules/angular2/test/transform/two_deps_files/bar.dart @@ -0,0 +1,13 @@ +library bar; + +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'foo.dart'; + +@Directive(context: const MyContext(contextString)) +class Component2 { + final MyContext c; + final String generatedValue; + Component2(this.c, String inValue) { + generatedValue = 'generated ' + inValue; + } +} 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 new file mode 100644 index 0000000000..8e38a7a82a --- /dev/null +++ b/modules/angular2/test/transform/two_deps_files/expected/index.bootstrap.dart @@ -0,0 +1,18 @@ +import 'package:angular2/src/reflection/reflection.dart' show reflector; +import 'bar.dart' as i0; +import 'foo.dart' as i1; +import 'package:angular2/src/core/annotations/annotations.dart' as i2; +import 'index.dart' as i3; + +main() { + reflector + ..registerType(i0.Component2, { + "factory": + (i1.MyContext c, String inValue) => new i0.Component2(c, inValue), + "parameters": const [const [i1.MyContext, String]], + "annotations": const [ + const i2.Directive(context: const i1.MyContext(i1.contextString)) + ] + }); + i3.main(); +} diff --git a/modules/angular2/test/transform/two_deps_files/foo.dart b/modules/angular2/test/transform/two_deps_files/foo.dart new file mode 100644 index 0000000000..3a081616fb --- /dev/null +++ b/modules/angular2/test/transform/two_deps_files/foo.dart @@ -0,0 +1,8 @@ +library foo; + +const contextString = 'soup'; + +class MyContext { + final String s; + const MyContext(this.s); +} diff --git a/modules/angular2/test/transform/two_deps_files/index.dart b/modules/angular2/test/transform/two_deps_files/index.dart new file mode 100644 index 0000000000..f65483f03e --- /dev/null +++ b/modules/angular2/test/transform/two_deps_files/index.dart @@ -0,0 +1,7 @@ +library web_foo; + +import 'bar.dart'; + +void main() { + new Component('Things'); +} diff --git a/modules/angular2/transformer.dart b/modules/angular2/transformer.dart new file mode 100644 index 0000000000..fdcc8187c5 --- /dev/null +++ b/modules/angular2/transformer.dart @@ -0,0 +1,3 @@ +library angular2.transformer_dart; + +export 'src/transform/transformer.dart'; diff --git a/modules/examples/pubspec.yaml b/modules/examples/pubspec.yaml index 05fd145e48..5bc3fd2ebd 100644 --- a/modules/examples/pubspec.yaml +++ b/modules/examples/pubspec.yaml @@ -8,6 +8,9 @@ dependencies: dev_dependencies: guinness: ">=0.1.16 <0.2.0" transformers: + - angular2: + entry_point: web/src/hello_world/index.dart + html_entry_point: web/src/hello_world/index.html - $dart2js: minify: true commandLineOptions: [--trust-type-annotations, --trust-primitives, --dump-info]