From 2d3b980570d4d115121adb519eeefc64514173a8 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 10 Mar 2014 09:14:40 -0700 Subject: [PATCH 1/3] First step of integrating transformers to angular. --- example/pubspec.lock | 23 +- example/pubspec.yaml | 9 + example/web/todo.dart | 8 - lib/auto_modules.dart | 64 +++ .../transformer/expression_generator.dart | 196 +++++++ lib/tools/transformer/metadata_extractor.dart | 340 ++++++++++++ lib/tools/transformer/metadata_generator.dart | 154 ++++++ lib/tools/transformer/options.dart | 58 ++ lib/transformer.dart | 129 +++++ pubspec.lock | 9 +- pubspec.yaml | 5 + .../expression_extractor_spec.dart | 81 +++ .../transformer/metadata_generator_spec.dart | 504 ++++++++++++++++++ 13 files changed, 1565 insertions(+), 15 deletions(-) create mode 100644 lib/auto_modules.dart create mode 100644 lib/tools/transformer/expression_generator.dart create mode 100644 lib/tools/transformer/metadata_extractor.dart create mode 100644 lib/tools/transformer/metadata_generator.dart create mode 100644 lib/tools/transformer/options.dart create mode 100644 lib/transformer.dart create mode 100644 test/tools/transformer/expression_extractor_spec.dart create mode 100644 test/tools/transformer/metadata_generator_spec.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index 08762dadd..6d13f3cd4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" angular: description: path: ".." @@ -15,17 +15,28 @@ packages: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" browser: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git version: "0.0.33" html5lib: description: html5lib @@ -34,11 +45,15 @@ packages: intl: description: intl source: hosted - version: "0.9.7" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0aff3ae1d..888a5bb11 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,3 +5,12 @@ dependencies: path: ../ browser: any unittest: any + +transformers: +- angular: + dart_entry: web/todo.dart + html_files: web/todo.html + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/example/web/todo.dart b/example/web/todo.dart index d3a5c674f..af69dc36a 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -7,14 +7,6 @@ import 'todo.dart'; import 'dart:html'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - class Item { String text; bool done; diff --git a/lib/auto_modules.dart b/lib/auto_modules.dart new file mode 100644 index 000000000..8b52d4ee9 --- /dev/null +++ b/lib/auto_modules.dart @@ -0,0 +1,64 @@ +/** + * Library to automatically switch between the dynamic injector and a static + * injector created by a pub build task. + * + * ## Step 1: Hook up the build step + * Edit ```pubspec.yaml``` to add the di transformer to the list of + * transformers. + * + * name: transformer_demo + * version: 0.0.1 + * dependencies: + * browser: any + * inject: any + * transformers: + * - angular: + * dart_entry: web/main.dart + * injectableAnnotations: NgInjectableService + * + * It's important to have the ```dart_entry``` entry to indicate the entry + * point of the application. + * + * By default, any classes or constructors annotated with @inject will be + * injected, but additional annotations can be specified with the annotations + * argument. + * + * ## Step 2: Annotate your types + * + * class Engine { + * @inject + * Engine(); + * } + * + * or + * + * @NgInjectableServide // custom annotation provided in pubspec.yaml + * class Car {} + * + * Note that all injectable classes must be in source files in lib/ directories. + * + * ## Step 3: Use the auto injector + * Modify your entry script to use the [defaultAutoInjector] as the injector, + * or alternatively the [defaultInjectorModule]. + * + * This must be done from the file registered as the dart_entry in pubspec.yaml + * as this is the only file which will be modified to include the generated + * injector. + */ +library angular.auto_modules; + +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; + + +@MirrorsUsed(override: '*') +import 'dart:mirrors' show MirrorsUsed; + +// Empty since the default is the dynamic expression module. +Module get defaultExpressionModule => new Module(); + +// Empty since the default is the dynamic metadata module. +Module get defaultMetadataModule => new Module(); + +// Empty since the default is the template cache module. +Module get defaultTemplateCacheModule => new Module(); diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart new file mode 100644 index 000000000..924526ee9 --- /dev/null +++ b/lib/tools/transformer/expression_generator.dart @@ -0,0 +1,196 @@ +library angular.tools.transformer.expression_generator; + +import 'dart:async'; +import 'dart:math' as math; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/core/module.dart'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/tools/html_extractor.dart'; +import 'package:angular/tools/parser_getter_setter/generator.dart'; +import 'package:angular/tools/source_crawler.dart'; +import 'package:angular/tools/source_metadata_extractor.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:path/path.dart' as path; + + +const String _generatedExpressionFilename = 'generated_static_expressions.dart'; + +/** + * Transformer which gathers all expressions from the HTML source files and + * Dart source files of an application and packages them for static evaluation. + * + * This will also modify the main Dart source file to import the generated + * expressions and modify all references to NG_EXPRESSION_MODULE to refer to + * the generated expressions. + */ +class ExpressionGenerator extends ResolverTransformer { + final TransformOptions options; + + ExpressionGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => options.isDartEntry(input.id); + + Future applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var outputBuffer = new StringBuffer(); + + _writeStaticExpressionHeader(asset.id, outputBuffer); + + var sourceMetadataExtractor = new SourceMetadataExtractor(); + var directives = + sourceMetadataExtractor.gatherDirectiveInfo(null, + new _LibrarySourceCrawler(resolver.libraries)); + + var htmlExtractor = new HtmlExpressionExtractor(directives); + return _getHtmlSources(transform) + .forEach(htmlExtractor.parseHtml) + .then((_) { + var module = new Module() + ..type(Parser, implementedBy: DynamicParser) + ..type(ParserBackend, implementedBy: DartGetterSetterGen); + var injector = + new DynamicInjector(modules: [module], allowImplicitInjection: true); + + injector.get(_ParserGetterSetter).generateParser( + htmlExtractor.expressions.toList(), outputBuffer); + + var outputId = + new AssetId(asset.id.package, 'lib/$_generatedExpressionFilename'); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + + transformIdentifiers(transform, resolver, + identifier: 'angular.auto_modules.defaultExpressionModule', + replacement: 'expressionModule', + importPrefix: 'generated_static_expressions', + importUrl: _generatedExpressionFilename); + }); + } + + /** + * Gets a stream consisting of the contents of all HTML source files to be + * scoured for expressions. + */ + Stream _getHtmlSources(Transform transform) { + var controller = new StreamController(); + if (options.htmlFiles == null) { + controller.close(); + return controller.stream; + } + Future.wait(options.htmlFiles.map((path) { + var htmlId = new AssetId(transform.primaryInput.id.package, path); + return transform.readInputAsString(htmlId); + }).map((future) { + return future.then(controller.add).catchError(controller.addError); + })).then((_) { + controller.close(); + }); + return controller.stream; + } +} + +void _writeStaticExpressionHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +'''); +} + +class _LibrarySourceCrawler implements SourceCrawler { + final List libraries; + _LibrarySourceCrawler(this.libraries); + + void crawl(String entryPoint, CompilationUnitVisitor visitor) { + libraries.expand((lib) => lib.units) + .map((compilationUnitElement) => compilationUnitElement.node) + .forEach(visitor); + } +} + +class _ParserGetterSetter { + final Parser parser; + final ParserBackend backend; + _ParserGetterSetter(this.parser, this.backend); + + generateParser(List exprs, StringSink sink) { + exprs.forEach((expr) { + try { + parser(expr); + } catch (e) { + // Ignore exceptions. + } + }); + + DartGetterSetterGen backend = this.backend; + sink.write(generateClosureMap(backend.properties, backend.calls)); + } + + String generateClosureMap(Set properties, + Map> calls) { + return ''' +class StaticClosureMap extends ClosureMap { + Map _getters = ${generateGetterMap(properties)}; + Map _setters = ${generateSetterMap(properties)}; + List> _functions = ${generateFunctionMap(calls)}; + + Getter lookupGetter(String name) + => _getters[name]; + Setter lookupSetter(String name) + => _setters[name]; + lookupFunction(String name, int arity) + => (arity < _functions.length) ? _functions[arity][name] : null; +} +'''; + } + + generateGetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); + return '{\n ${lines.join(",\n ")}\n }'; + } + + generateSetterMap(Iterable keys) { + var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); + return '{\n ${lines.join(",\n ")}\n }'; + } + + generateFunctionMap(Map> calls) { + Map> arities = {}; + calls.forEach((name, callArities) { + callArities.forEach((arity){ + arities.putIfAbsent(arity, () => new Set()).add(name); + }); + }); + + var maxArity = arities.isEmpty ? 0 : + arities.keys.reduce((x, y) => math.max(x, y)); + + var maps = new Iterable.generate(maxArity, (arity) { + var names = arities[arity]; + if (names == null) { + return '{\n }'; + } else { + var args = new List.generate(arity, (e) => "a$e").join(','); + var p = args.isEmpty ? '' : ', $args'; + var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); + return '{\n ${lines.join(",\n ")}\n }'; + } + }); + + return '[${maps.join(",")}]'; + } +} + diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart new file mode 100644 index 000000000..fefcf25e4 --- /dev/null +++ b/lib/tools/transformer/metadata_extractor.dart @@ -0,0 +1,340 @@ +library angular.metadata_extractor; + +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'; + +class AnnotatedType { + final ClassElement type; + Iterable annotations; + + final Map members = {}; + + AnnotatedType(this.type); + + /** + * Finds all the libraries referenced by the annotations + */ + Iterable get referencedLibraries { + var libs = new Set(); + libs.add(type.library); + + var libCollector = new _LibraryCollector(); + for (var annotation in annotations) { + annotation.accept(libCollector); + } + for (var annotation in members.values) { + annotation.accept(libCollector); + } + libs.addAll(libCollector.libraries); + + return libs; + } + + void writeClassAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + sink.write(' ${prefixes[type.library]}${type.name}: [\n'); + var writer = new _AnnotationWriter(sink, prefixes); + for (var annotation in annotations) { + sink.write(' '); + if (writer.writeAnnotation(annotation)) { + sink.write(',\n'); + } else { + sink.write('null,\n'); + logger.warning('Unable to serialize annotation $annotation.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + } + } + sink.write(' ],\n'); + } + + void writeMemberAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + if (members.isEmpty) return; + + sink.write(' ${prefixes[type.library]}${type.name}: {\n'); + + var writer = new _AnnotationWriter(sink, prefixes); + members.forEach((memberName, annotation) { + sink.write(' \'$memberName\': '); + if (writer.writeAnnotation(annotation)) { + sink.write(',\n'); + } else { + sink.write('null,\n'); + logger.warning('Unable to serialize annotation $annotation.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + } + }); + sink.write(' },\n'); + } +} + +/** + * Helper which finds all libraries referenced within the provided AST. + */ +class _LibraryCollector extends GeneralizingASTVisitor { + final Set libraries = new Set(); + void visitSimpleIdentifier(SimpleIdentifier s) { + var element = s.bestElement; + if (element != null) { + libraries.add(element.library); + } + } +} + +/** + * Helper class which writes annotations out to the buffer. + * This does not support every syntax possible, but will return false when + * the annotation cannot be serialized. + */ +class _AnnotationWriter { + final StringBuffer sink; + final Map prefixes; + + _AnnotationWriter(this.sink, this.prefixes); + + /** + * Returns true if the annotation was successfully serialized. + * If the annotation could not be written then the buffer is returned to its + * original state. + */ + bool writeAnnotation(Annotation annotation) { + // Record the current location in the buffer and if writing fails then + // back up the buffer to where we started. + var len = sink.length; + if (!_writeAnnotation(annotation)) { + var str = sink.toString(); + sink.clear(); + sink.write(str.substring(0, len)); + return false; + } + return true; + } + + bool _writeAnnotation(Annotation annotation) { + var element = annotation.element; + if (element is ConstructorElement) { + sink.write('const ${prefixes[element.library]}' + '${element.enclosingElement.name}'); + // Named constructors + if (!element.name.isEmpty) { + sink.write('.${element.name}'); + } + sink.write('('); + if (!_writeArguments(annotation)) return false; + sink.write(')'); + return true; + } else if (element is PropertyAccessorElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + + return false; + } + + /** Writes the arguments for a type constructor. */ + bool _writeArguments(Annotation annotation) { + var args = annotation.arguments; + var index = 0; + for (var arg in args.arguments) { + if (arg is NamedExpression) { + sink.write('${arg.name.label.name}: '); + if (!_writeExpression(arg.expression)) return false; + } else { + if (!_writeExpression(arg)) return false; + } + if (++index < args.arguments.length) { + sink.write(', '); + } + } + return true; + } + + /** Writes an expression. */ + bool _writeExpression(Expression expression) { + if (expression is StringLiteral) { + var str = expression.stringValue + .replaceAll(r'\', r'\\') + .replaceAll('\'', '\\\''); + sink.write('\'$str\''); + return true; + } + if (expression is ListLiteral) { + sink.write('const ['); + for (var element in expression.elements) { + if (!_writeExpression(element)) return false; + sink.write(','); + } + sink.write(']'); + return true; + } + if (expression is MapLiteral) { + sink.write('const {'); + var index = 0; + for (var entry in expression.entries) { + if (!_writeExpression(entry.key)) return false; + sink.write(': '); + if (!_writeExpression(entry.value)) return false; + if (++index < expression.entries.length) { + sink.write(', '); + } + } + sink.write('}'); + return true; + } + if (expression is Identifier) { + var element = expression.bestElement; + if (element == null || !element.isPublic) return false; + + if (element is ClassElement) { + sink.write('${prefixes[element.library]}${element.name}'); + return true; + } + if (element is PropertyAccessorElement) { + var variable = element.variable; + if (variable is FieldElement) { + var cls = variable.enclosingElement; + sink.write('${prefixes[cls.library]}${cls.name}.${variable.name}'); + return true; + } else if (variable is TopLevelVariableElement) { + sink.write('${prefixes[variable.library]}${variable.name}'); + return true; + } + print('variable ${variable.runtimeType} $variable'); + } + print('element ${element.runtimeType} $element'); + } + if (expression is BooleanLiteral) { + sink.write(expression.value); + return true; + } + if (expression is DoubleLiteral) { + sink.write(expression.value); + return true; + } + if (expression is IntegerLiteral) { + sink.write(expression.value); + return true; + } + if (expression is NullLiteral) { + sink.write('null'); + return true; + } + print('expression ${expression.runtimeType} $expression'); + return false; + } +} + +class AnnotationExtractor { + final TransformLogger logger; + final Resolver resolver; + + static const List _angularAnnotationNames = const [ + 'angular.core.NgAttr', + 'angular.core.NgOneWay', + 'angular.core.NgOneWayOneTime', + 'angular.core.NgTwoWay', + 'angular.core.NgCallback' + ]; + + /// Resolved annotations that this will pick up for members. + final List _annotationElements = []; + + AnnotationExtractor(this.logger, this.resolver) { + for (var annotation in _angularAnnotationNames) { + var type = resolver.getType(annotation); + if (type == null) { + logger.warning('Unable to resolve $annotation, skipping metadata.'); + continue; + } + _annotationElements.add(type.unnamedConstructor); + } + } + + AnnotatedType extractAnnotations(ClassElement cls) { + if (resolver.getImportUri(cls.library) == null) { + logger.warning('Dropping annotations for ${cls.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', + asset: resolver.getSourceAssetId(cls), + span: resolver.getSourceSpan(cls)); + return null; + } + + var visitor = new _AnnotationVisitor(_annotationElements); + cls.node.accept(visitor); + + if (!visitor.hasAnnotations) return null; + + var type = new AnnotatedType(cls); + type.annotations = visitor.classAnnotations + .where((annotation) { + var element = annotation.element; + if (element != null && !element.isPublic) { + logger.warning('Annotation $annotation is not public.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + return false; + } + if (element is ConstructorElement && + !element.enclosingElement.isPublic) { + logger.warning('Annotation $annotation is not public.', + asset: resolver.getSourceAssetId(annotation.parent.element), + span: resolver.getSourceSpan(annotation.parent.element)); + return false; + } + return true; + }).toList(); + + + visitor.memberAnnotations.forEach((memberName, annotations) { + if (annotations.length > 1) { + logger.warning('$memberName can only have one annotation.', + asset: resolver.getSourceAssetId(annotations[0].parent.element), + span: resolver.getSourceSpan(annotations[0].parent.element)); + return; + } + + type.members[memberName] = annotations[0]; + }); + + if (type.annotations.isEmpty && type.members.isEmpty) return null; + + return type; + } +} + + +/** + * AST visitor which walks the current AST and finds all annotated + * classes and members. + */ +class _AnnotationVisitor extends GeneralizingASTVisitor { + final List allowedMemberAnnotations; + final List classAnnotations = []; + final Map> memberAnnotations = {}; + + _AnnotationVisitor(this.allowedMemberAnnotations); + + void visitAnnotation(Annotation annotation) { + var parent = annotation.parent; + if (parent is! Declaration) return; + + if (parent.element is ClassElement) { + classAnnotations.add(annotation); + } else if (allowedMemberAnnotations.contains(annotation.element)) { + if (parent is MethodDeclaration) { + memberAnnotations.putIfAbsent(parent.name.name, () => []) + .add(annotation); + } else if (parent is FieldDeclaration) { + var name = parent.fields.variables.first.name.name; + memberAnnotations.putIfAbsent(name, () => []).add(annotation); + } + } + } + + bool get hasAnnotations => + !classAnnotations.isEmpty || !memberAnnotations.isEmpty; +} diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart new file mode 100644 index 000000000..ca0fb419a --- /dev/null +++ b/lib/tools/transformer/metadata_generator.dart @@ -0,0 +1,154 @@ +library angular.tools.transformer.metadata_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:path/path.dart' as path; + +import 'metadata_extractor.dart'; + +const String _generatedMetadataFilename = 'generated_metadata.dart'; + +class MetadataGenerator extends ResolverTransformer { + final TransformOptions options; + + MetadataGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + var extractor = new AnnotationExtractor(transform.logger, resolver); + + var outputBuffer = new StringBuffer(); + _writeHeader(asset.id, outputBuffer); + + var annotatedTypes = resolver.libraries + .where((lib) => !lib.isInSdk) + .expand((lib) => lib.units) + .expand((unit) => unit.types) + .map(extractor.extractAnnotations) + .where((annotations) => annotations != null).toList(); + + var libs = annotatedTypes.expand((type) => type.referencedLibraries) + .toSet(); + + var importPrefixes = {}; + var index = 0; + for (var lib in libs) { + if (lib.isDartCore) { + importPrefixes[lib] = ''; + continue; + } + + var prefix = 'import_${index++}'; + var url = resolver.getImportUri(lib); + outputBuffer.write('import \'$url\' as $prefix;\n'); + importPrefixes[lib] = '$prefix.'; + } + + _writePreamble(outputBuffer); + + _writeClassPreamble(outputBuffer); + for (var type in annotatedTypes) { + type.writeClassAnnotations( + outputBuffer, transform.logger, resolver, importPrefixes); + } + _writeClassEpilogue(outputBuffer); + + _writeMemberPreamble(outputBuffer); + for (var type in annotatedTypes) { + type.writeMemberAnnotations( + outputBuffer, transform.logger, resolver, importPrefixes); + } + _writeMemberEpilogue(outputBuffer); + + var outputId = + new AssetId(asset.id.package, 'lib/$_generatedMetadataFilename'); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + + transformIdentifiers(transform, resolver, + identifier: 'angular.auto_modules.defaultMetadataModule', + replacement: 'metadataModule', + importPrefix: 'generated_metadata', + importUrl: _generatedMetadataFilename); + } +} + +void _writeHeader(AssetId id, StringSink sink) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_metadata; + +import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:di/di.dart' show Module; + +'''); +} + +void _writePreamble(StringSink sink) { + sink.write(''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()) + ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = _classAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { + Map call(Type type) { + var annotations = _memberAnnotations[type]; + if (annotations != null) { + return annotations; + } + return {}; + } +} + +'''); +} + +void _writeClassPreamble(StringSink sink) { + sink.write(''' +final Map _classAnnotations = { +'''); +} + +void _writeClassEpilogue(StringSink sink) { + sink.write(''' +}; +'''); +} + +void _writeMemberPreamble(StringSink sink) { + sink.write(''' + +final Map> _memberAnnotations = { +'''); +} + +void _writeMemberEpilogue(StringSink sink) { + sink.write(''' +}; +'''); +} + +void _writeFooter(StringSink sink) { + sink.write(''' +}; +'''); +} diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart new file mode 100644 index 000000000..3fe347db6 --- /dev/null +++ b/lib/tools/transformer/options.dart @@ -0,0 +1,58 @@ +library angular.tools.transformer.options; + +import 'dart:async'; + +import 'package:barback/barback.dart'; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + +/** Options used by Angular transformers */ +class TransformOptions { + + /** + * The file path of the primary Dart entry point (main) for the application. + * This is used as the starting point to find all expressions used by the + * application. + */ + final String dartEntry; + + /** + * List of html file paths which may contain Angular expressions. + * The paths are relative to the package home and are represented using posix + * style, which matches the representation used in asset ids in barback. + */ + final List htmlFiles; + + /** + * Path to the Dart SDK directory, for resolving Dart libraries. + */ + final String sdkDirectory; + + /** + * Template cache path modifiers + */ + final Map templateUriRewrites; + + /** + * Dependency injection options. + */ + final di.TransformOptions diOptions; + + TransformOptions({String dartEntry, + String sdkDirectory, List htmlFiles, + Map templateUriRewrites, + di.TransformOptions diOptions}) + : dartEntry = dartEntry, + sdkDirectory = sdkDirectory, + htmlFiles = htmlFiles != null ? htmlFiles : [], + templateUriRewrites = templateUriRewrites != null ? + templateUriRewrites : {}, + diOptions = diOptions { + if (sdkDirectory == null) + throw new ArgumentError('sdkDirectory must be provided.'); + } + + // Don't need to check package as transformers only run for primary package. + Future isDartEntry(AssetId id) => + new Future.value(id.path == dartEntry || dartEntry == '*'); +} diff --git a/lib/transformer.dart b/lib/transformer.dart new file mode 100644 index 000000000..977c8cbb5 --- /dev/null +++ b/lib/transformer.dart @@ -0,0 +1,129 @@ +library angular.transformer; + +import 'dart:io'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/injector_generator.dart' as di; +import 'package:di/transformer/options.dart' as di; +import 'package:path/path.dart' as path; + + + /** + * The Angular transformer, which internally runs several phases that will: + * + * * Extract all expressions for evaluation at runtime without using Mirrors. + * * Extract all classes being dependency injected into a static injector. + * * Extract all metadata for cached reflection. + */ +class AngularTransformerGroup implements TransformerGroup { + final Iterable phases; + + AngularTransformerGroup(TransformOptions options) + : phases = _createPhases(options); + + AngularTransformerGroup.asPlugin(BarbackSettings settings) + : this(_parseSettings(settings.configuration)); +} + +TransformOptions _parseSettings(Map args) { + // Default angular annotations for injectable types + var annotations = [ + 'angular.core.service.NgInjectableService', + 'angular.core.NgDirective', + 'angular.core.NgController', + 'angular.core.NgComponent', + 'angular.core.NgFilter']; + annotations.addAll(_readStringListValue(args, 'injectable_annotations')); + + var injectedTypes = ['perf_api.Profiler', + 'angular.core.parser.static_parser.StaticParser']; + injectedTypes.addAll(_readStringListValue(args, 'injected_types')); + + var sdkDir = _readStringValue(args, 'dart_sdk', required: false); + if (sdkDir == null) { + // Assume the Pub executable is always coming from the SDK. + sdkDir = path.dirname(path.dirname(Platform.executable)); + } + + var dartEntry = _readStringValue(args, 'dart_entry'); + + var diOptions = new di.TransformOptions( + dartEntries: [dartEntry], + injectableAnnotations: annotations, + injectedTypes: injectedTypes, + sdkDirectory: sdkDir); + + return new TransformOptions( + dartEntry: dartEntry, + htmlFiles: _readStringListValue(args, 'html_files'), + sdkDirectory: sdkDir, + templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), + diOptions: diOptions); +} + +_readStringValue(Map args, String name, {bool required: true}) { + var value = args[name]; + if (value == null) { + if (required) { + print('Angular transformer parameter "$name" ' + 'has no value in pubspec.yaml.'); + } + return null; + } + if (value is! String) { + print('Angular transformer parameter "$name" value ' + 'is not a string in pubspec.yaml.'); + return null; + } + return value; +} + +_readStringListValue(Map args, String name) { + var value = args[name]; + if (value == null) return []; + var results = []; + bool error; + if (value is List) { + results = value; + error = value.any((e) => e is! String); + } else if (value is String) { + results = [value]; + error = false; + } else { + error = true; + } + if (error) { + print('Angular transformer parameter "$name" ' + 'has an invalid value in pubspec.yaml.'); + } + return results; +} + +Map _readStringMapValue(Map args, String name) { + var value = args[name]; + if (value == null) return {}; + if (value is! Map) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map parameter.'); + return {}; + } + if (value.keys.any((e) => e is! String) || + value.values.any((e) => e is! String)) { + print('Angular transformer parameter "$name" ' + 'is expected to be a map of strings.'); + return {}; + } + return value; +} + +List> _createPhases(TransformOptions options) { + var resolvers = new Resolvers(options.sdkDirectory); + return [ + [new ExpressionGenerator(options, resolvers)], + [new di.InjectorGenerator(options.diOptions, resolvers)], + [new MetadataGenerator(options, resolvers)], + ]; +} diff --git a/pubspec.lock b/pubspec.lock index 07b511f97..0a9745430 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,9 +30,12 @@ packages: source: hosted version: "0.9.1" di: - description: di - source: hosted - version: "0.0.34" + description: + ref: null + resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" + url: "https://github.com/angular/di.dart.git" + source: git + version: "0.0.33" html5lib: description: html5lib source: hosted diff --git a/pubspec.yaml b/pubspec.yaml index 63bbe5173..c8a4a4e22 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ environment: dependencies: analyzer: '>=0.12.0 <0.13.0' browser: '>=0.8.7 <0.10.0' + code_transformers: '>=0.0.1-dev.2 <0.1.0' collection: '>=0.9.1 <1.0.0' di: '>=0.0.32 <0.1.0' html5lib: '>=0.8.7 <0.10.0' @@ -24,3 +25,7 @@ dependencies: dev_dependencies: benchmark_harness: '>=1.0.0' unittest: '>=0.8.7 <0.10.0' + +dependency_overrides: + di: + git: https://github.com/angular/di.dart.git diff --git a/test/tools/transformer/expression_extractor_spec.dart b/test/tools/transformer/expression_extractor_spec.dart new file mode 100644 index 000000000..de7a68f9e --- /dev/null +++ b/test/tools/transformer/expression_extractor_spec.dart @@ -0,0 +1,81 @@ +library angular.test.tools.transformer.expression_extractor_spec; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; +import '../../jasmine_syntax.dart'; + +main() { + describe('expression_extractor', () { + var htmlFiles = []; + var options = new TransformOptions( + dartEntry: 'web/main.dart', + htmlFiles: htmlFiles, + sdkDirectory: dartSdkDirectory); + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new ExpressionGenerator(options, resolvers)] + ]; + + it('should extract expressions', () { + htmlFiles.add('web/index.html'); + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/auto_modules.dart': PACKAGE_AUTO, + 'a|web/main.dart': ''' +library foo; +import 'package:angular/auto_modules.dart'; +''', + 'a|web/index.html': ''' +
{{some.getter}}
+''' + }, + results: { + 'a|lib/generated_static_expressions.dart': ''' +$HEADER + Map _getters = { + r"some": (o) => o.some, + r"getter": (o) => o.getter + }; + Map _setters = { + r"some": (o, v) => o.some = v, + r"getter": (o, v) => o.getter = v + }; + List> _functions = []; +$FOOTER +''' + }).then((_) { + htmlFiles.clear(); + }); + }); + }); +} + +const String HEADER = ''' +library a.web.main.generated_expressions; + +import 'package:angular/angular.dart'; +import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; + +Module get expressionModule => new Module() + ..value(ClosureMap, new StaticClosureMap()); + +class StaticClosureMap extends ClosureMap {'''; + +const String FOOTER = ''' + + Getter lookupGetter(String name) + => _getters[name]; + Setter lookupSetter(String name) + => _setters[name]; + lookupFunction(String name, int arity) + => (arity < _functions.length) ? _functions[arity][name] : null; +}'''; + +const String PACKAGE_AUTO = ''' +library angular.auto_modules; + +Module get defaultExpressionModule => new Module(); +'''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart new file mode 100644 index 000000000..69528c504 --- /dev/null +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -0,0 +1,504 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('metadata_generator', () { + var options = new TransformOptions( + dartEntry: 'web/main.dart', + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new MetadataGenerator(options, resolvers)] + ]; + + it('should extract member metadata', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(selector: r'[*=/{{.*}}/]') + @proxy + class Engine { + @NgOneWay('another-expression') + String anotherExpression; + + @NgCallback('callback') + set callback(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\')', + 'proxy', + ] + }, + classMembers: { + 'import_0.Engine': { + 'anotherExpression': 'const import_1.NgOneWay(\'another-expression\')', + 'callback': 'const import_1.NgCallback(\'callback\')', + 'twoWayStuff': 'const import_1.NgTwoWay(\'two-way-stuff\')', + } + }); + }); + + it('should warn on un-importable files', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|web/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(selector: r'[*=/{{.*}}/]') + class Engine {} + ''' + }, + messages: ['warning: Dropping annotations for Engine because the ' + 'containing file cannot be imported (must be in a lib folder). ' + '(a.dart 2 16)']); + }); + + it('should warn on multiple annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(package:a/a.dart 3 18)']); + }); + + it('should warn on multiple annotations (across getter/setter)', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + set callback(Function) {} + + @NgOneWay('another-expression') + get callback() {} + } + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(package:a/a.dart 3 18)']); + }); + + it('should extract map arguments', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {\'ng-value\': \'&ngValue\', \'key\': \'value\'})', + ] + }); + }); + + it('should extract list arguments', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(publishTypes: const [TextChangeListener]) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_1.TextChangeListener,])', + ] + }); + }); + + it('should extract primitive literals', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgOneWay(true) + @NgOneWay(1.0) + @NgOneWay(1) + @NgOneWay(null) + class Engine {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgOneWay(true)', + 'const import_1.NgOneWay(1.0)', + 'const import_1.NgOneWay(1)', + 'const import_1.NgOneWay(null)', + ] + }); + }); + + it('should skip and warn on unserializable annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @Foo + class Engine {} + + @NgDirective(publishTypes: const [Foo]) + class Car {} + ''' + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'null', + ], + 'import_0.Car': [ + 'null', + ] + }, + messages: [ + 'warning: Unable to serialize annotation @Foo. ' + '(package:a/a.dart 2 16)', + 'warning: Unable to serialize annotation ' + '@NgDirective(publishTypes: const [Foo]). ' + '(package:a/a.dart 5 16)', + ]); + }); + + it('should extract types across libs', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + import 'package:a/b.dart'; + + @NgDirective(publishTypes: const [Car]) + class Engine {} + ''', + 'a|lib/b.dart': ''' + class Car {} + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + 'import \'package:a/b.dart\' as import_2;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(publishTypes: const [import_2.Car,])', + ] + }); + }); + + it('should not gather non-member annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + Engine() { + @NgDirective() + print('something'); + } + } + ''', + }); + }); + + it('properly escapes strings', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': r''' + import 'package:angular/angular.dart'; + + @NgOneWay('foo\' \\') + class Engine { + } + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + r'''const import_1.NgOneWay('foo\' \\')''', + ] + }); + }); + + it('should reference static and global properties', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) + @NgDirective(visibility: CONST_VALUE) + class Engine {} + + const int CONST_VALUE = 2; + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + '''const import_1.NgDirective(visibility: import_1.NgDirective.CHILDREN_VISIBILITY)''', + '''const import_1.NgDirective(visibility: import_0.CONST_VALUE)''', + ] + }); + }); + + it('should not extract private annotations', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @_Foo() + @_foo + class Engine { + } + + class _Foo { + const _Foo(); + } + const _Foo _foo = const _Foo(); + ''', + }, + messages: [ + 'warning: Annotation @_Foo() is not public. ' + '(package:a/a.dart 2 16)', + 'warning: Annotation @_foo is not public. (package:a/a.dart 2 16)', + ]); + }); + + it('supports named constructors', () { + return generates(phases, + inputs: { + 'a|web/main.dart': '''import 'package:a/a.dart'; ''', + 'angular|lib/angular.dart': PACKAGE_ANGULAR, + 'a|lib/a.dart': ''' + import 'package:angular/angular.dart'; + + @Foo.bar() + @Foo._private() + class Engine { + } + + class Foo { + const Foo.bar(); + const Foo._private(); + } + ''', + }, + imports: [ + 'import \'package:a/a.dart\' as import_0;', + ], + classes: { + 'import_0.Engine': [ + '''const import_0.Foo.bar()''', + ] + }, + messages: [ + 'warning: Annotation @Foo._private() is not public. ' + '(package:a/a.dart 2 16)', + ]); + }); + }); +} + +Future generates(List> phases, + {Map inputs, Iterable imports: const [], + Map classes: const {}, + Map classMembers: const {}, + Iterable messages: const []}) { + + var buffer = new StringBuffer(); + buffer.write('$HEADER\n'); + for (var i in imports) { + buffer.write('$i\n'); + } + buffer.write('$BOILER_PLATE\n'); + for (var className in classes.keys) { + buffer.write(' $className: [\n'); + for (var annotation in classes[className]) { + buffer.write(' $annotation,\n'); + } + buffer.write(' ],\n'); + } + buffer.write('$MEMBER_PREAMBLE\n'); + for (var className in classMembers.keys) { + buffer.write(' $className: {\n'); + var members = classMembers[className]; + for (var memberName in members.keys) { + buffer.write(' \'$memberName\': ${members[memberName]},\n'); + } + buffer.write(' },\n'); + } + + buffer.write('$FOOTER\n'); + + return tests.applyTransformers(phases, + inputs: inputs, + results: { + 'a|lib/generated_metadata.dart': buffer.toString() + }, + messages: messages); +} + +const String HEADER = ''' +library a.web.main.generated_metadata; + +import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:di/di.dart' show Module; +'''; + +const String BOILER_PLATE = ''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()) + ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = _classAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { + Map call(Type type) { + var annotations = _memberAnnotations[type]; + if (annotations != null) { + return annotations; + } + return {}; + } +} + +final Map _classAnnotations = {'''; + +const String MEMBER_PREAMBLE = ''' +}; + +final Map> _memberAnnotations = {'''; + +const String FOOTER = ''' +};'''; + + +const String PACKAGE_ANGULAR = ''' +library angular.core; + +class NgDirective { + const NgDirective({selector, publishTypes, map, visibility}); + + static const int CHILDREN_VISIBILITY = 1; +} + +class NgOneWay { + const NgOneWay(arg); +} + +class NgTwoWay { + const NgTwoWay(arg); +} + +class NgCallback { + const NgCallback(arg); +} + +class NgAttr { + const NgAttr(); +} +class NgOneWayOneTime { + const NgOneWayOneTime(arg); +} + +class TextChangeListener {} +'''; From 84916a1018bfc20e5090da03617afad0ba4e466a Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 10 Mar 2014 18:46:29 -0700 Subject: [PATCH 2/3] Adding static transformers This adds transformers which generate code for metadata and expressions and switches an app from NgDynamicApp to NgStaticApp. This is currently only configured to run on the Todo & BouncingBalls examples. --- example/pubspec.yaml | 8 +- example/web/bouncing_balls.dart | 76 +----- example/web/todo.dart | 3 +- example/web/todo.html | 2 +- lib/auto_modules.dart | 64 ----- lib/core/registry_dynamic.dart | 1 - lib/mock/http_backend.dart | 10 +- lib/mock/module.dart | 9 +- lib/mock/test_injection.dart | 7 +- lib/playback/playback_http.dart | 2 +- .../transformer/expression_generator.dart | 40 ++- lib/tools/transformer/metadata_extractor.dart | 173 ++++++++---- lib/tools/transformer/metadata_generator.dart | 67 ++--- lib/tools/transformer/options.dart | 10 +- .../transformer/static_angular_generator.dart | 101 +++++++ lib/transformer.dart | 15 +- test/_specs.dart | 2 + test/jasmine_syntax.dart | 2 +- test/tools/transformer/all.dart | 11 + ...ec.dart => expression_generator_spec.dart} | 53 ++-- .../transformer/metadata_generator_spec.dart | 246 +++++++++--------- .../static_angular_generator_spec.dart | 111 ++++++++ 22 files changed, 577 insertions(+), 436 deletions(-) delete mode 100644 lib/auto_modules.dart create mode 100644 lib/tools/transformer/static_angular_generator.dart create mode 100644 test/tools/transformer/all.dart rename test/tools/transformer/{expression_extractor_spec.dart => expression_generator_spec.dart} (57%) create mode 100644 test/tools/transformer/static_angular_generator_spec.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 888a5bb11..3e0e3b133 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -8,8 +8,12 @@ dependencies: transformers: - angular: - dart_entry: web/todo.dart - html_files: web/todo.html + dart_entries: + - web/todo.dart + - web/bouncing_balls.dart + html_files: # Need to split out to per-entry assets + - web/todo.html + - web/bouncing_balls.html dependency_overrides: di: diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index d2ea4024f..c5bdd3437 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -1,6 +1,6 @@ import 'package:perf_api/perf_api.dart'; import 'package:angular/angular.dart'; -import 'package:angular/angular_static.dart'; +import 'package:angular/angular_dynamic.dart'; import 'package:angular/change_detection/change_detection.dart'; import 'dart:html'; import 'dart:math'; @@ -125,79 +125,7 @@ class MyModule extends Module { } main() { - var getters = { - 'x': (o) => o.x, - 'y': (o) => o.y, - 'bounce': (o) => o.bounce, - 'fps': (o) => o.fps, - 'balls': (o) => o.balls, - 'length': (o) => o.length, - 'digestTime': (o) => o.digestTime, - 'ballClassName': (o) => o.ballClassName, - 'position': (o) => o.position, - 'onClick': (o) => o.onClick, - 'ball': (o) => o.ball, - 'color': (o) => o.color, - 'changeCount': (o) => o.changeCount, - 'playPause': (o) => o.playPause, - 'toggleCSS': (o) => o.toggleCSS, - 'timeDigest': (o) => o.timeDigest, - 'expression': (o) => o.expression, - }; - var setters = { - 'position': (o, v) => o.position = v, - 'onClick': (o, v) => o.onClick = v, - 'ball': (o, v) => o.ball = v, - 'x': (o, v) => o.x = v, - 'y': (o, v) => o.y = v, - 'balls': (o, v) => o.balls = v, - 'bounce': (o, v) => o.bounce = v, - 'expression': (o, v) => o.expression = v, - 'fps': (o, v) => o.fps = v, - 'length': (o, v) => o.length = v, - 'digestTime': (o, v) => o.digestTime = v, - 'ballClassName': (o, v) => o.ballClassName = v, - }; - var metadata = { - BounceController: [new NgController(selector: '[bounce-controller]', publishAs: 'bounce')], - BallPositionDirective: [new NgDirective(selector: '[ball-position]', map: const { "ball-position": '=>position'})], - NgEventDirective: [new NgDirective(selector: '[ng-click]', map: const {'ng-click': '&onClick'})], - NgADirective: [new NgDirective(selector: 'a[href]')], - NgRepeatDirective: [new NgDirective(children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: '[ng-repeat]', map: const {'.': '@expression'})], - NgTextMustacheDirective: [new NgDirective(selector: r':contains(/{{.*}}/)')], - NgAttrMustacheDirective: [new NgDirective(selector: r'[*=/{{.*}}/]')], - }; - var types = { - Profiler: (t) => new Profiler(), - DirectiveSelectorFactory: (t) => new DirectiveSelectorFactory(), - DirectiveMap: (t) => new DirectiveMap(t(Injector), t(MetadataExtractor), t(DirectiveSelectorFactory)), - Lexer: (t) => new Lexer(), - ClosureMap: (t) => new StaticClosureMap(getters, setters), // TODO: types don't match - DynamicParserBackend: (t) => new DynamicParserBackend(t(ClosureMap)), - DynamicParser: (t) => new DynamicParser(t(Lexer), t(ParserBackend)), - Compiler: (t) => new Compiler(t(Profiler), t(Parser), t(Expando)), - WalkingCompiler: (t) => new WalkingCompiler(t(Profiler), t(Expando)), - DirectiveSelectorFactory: (t) => new DirectiveSelectorFactory(t(ElementBinderFactory)), - ElementBinderFactory: (t) => new ElementBinderFactory(t(Parser), t(Profiler), t(Expando)), - EventHandler: (t) => new EventHandler(t(Node), t(Expando), t(ExceptionHandler)), - AstParser: (t) => new AstParser(t(Parser)), - FilterMap: (t) => new FilterMap(t(Injector), t(MetadataExtractor)), - ExceptionHandler: (t) => new ExceptionHandler(), - FieldGetterFactory: (t) => new StaticFieldGetterFactory(getters), - ScopeDigestTTL: (t) => new ScopeDigestTTL(), - ScopeStats: (t) => new ScopeStats(), - RootScope: (t) => new RootScope(t(Object), t(AstParser), t(Parser), t(FieldGetterFactory), t(FilterMap), t(ExceptionHandler), t(ScopeDigestTTL), t(NgZone), t(ScopeStats)), - NgAnimate: (t) => new NgAnimate(), - Interpolate: (t) => new Interpolate(t(Parser)), - - NgEventDirective: (t) => new NgEventDirective(t(Element), t(Scope)), - NgADirective: (t) => new NgADirective(t(Element)), - NgRepeatDirective: (t) => new NgRepeatDirective(t(ViewPort), t(BoundViewFactory), t(Scope), t(Parser), t(AstParser), t(FilterMap)), - - BounceController: (t) => new BounceController(t(Scope)), - BallPositionDirective: (t) => new BallPositionDirective(t(Element), t(Scope)), - }; - ngStaticApp(types, metadata, getters, setters) + new NgDynamicApp() .addModule(new MyModule()) .run(); } diff --git a/example/web/todo.dart b/example/web/todo.dart index af69dc36a..01b6fa2eb 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -3,7 +3,6 @@ library todo; import 'package:angular/angular.dart'; import 'package:angular/angular_dynamic.dart'; import 'package:angular/playback/playback_http.dart'; -import 'todo.dart'; import 'dart:html'; @@ -32,6 +31,7 @@ abstract class ServerController { // An implementation of ServerController that does nothing. +@NgInjectableService() class NoServerController implements ServerController { init(TodoController todo) { } } @@ -39,6 +39,7 @@ class NoServerController implements ServerController { // An implementation of ServerController that fetches items from // the server over HTTP. +@NgInjectableService() class HttpServerController implements ServerController { final Http _http; HttpServerController(this._http); diff --git a/example/web/todo.html b/example/web/todo.html index aeeed8732..cbd32cc92 100644 --- a/example/web/todo.html +++ b/example/web/todo.html @@ -5,7 +5,7 @@ Things To Do - + diff --git a/lib/auto_modules.dart b/lib/auto_modules.dart deleted file mode 100644 index 8b52d4ee9..000000000 --- a/lib/auto_modules.dart +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Library to automatically switch between the dynamic injector and a static - * injector created by a pub build task. - * - * ## Step 1: Hook up the build step - * Edit ```pubspec.yaml``` to add the di transformer to the list of - * transformers. - * - * name: transformer_demo - * version: 0.0.1 - * dependencies: - * browser: any - * inject: any - * transformers: - * - angular: - * dart_entry: web/main.dart - * injectableAnnotations: NgInjectableService - * - * It's important to have the ```dart_entry``` entry to indicate the entry - * point of the application. - * - * By default, any classes or constructors annotated with @inject will be - * injected, but additional annotations can be specified with the annotations - * argument. - * - * ## Step 2: Annotate your types - * - * class Engine { - * @inject - * Engine(); - * } - * - * or - * - * @NgInjectableServide // custom annotation provided in pubspec.yaml - * class Car {} - * - * Note that all injectable classes must be in source files in lib/ directories. - * - * ## Step 3: Use the auto injector - * Modify your entry script to use the [defaultAutoInjector] as the injector, - * or alternatively the [defaultInjectorModule]. - * - * This must be done from the file registered as the dart_entry in pubspec.yaml - * as this is the only file which will be modified to include the generated - * injector. - */ -library angular.auto_modules; - -import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; - - -@MirrorsUsed(override: '*') -import 'dart:mirrors' show MirrorsUsed; - -// Empty since the default is the dynamic expression module. -Module get defaultExpressionModule => new Module(); - -// Empty since the default is the dynamic metadata module. -Module get defaultMetadataModule => new Module(); - -// Empty since the default is the template cache module. -Module get defaultTemplateCacheModule => new Module(); diff --git a/lib/core/registry_dynamic.dart b/lib/core/registry_dynamic.dart index 37de9cd00..49fff9537 100644 --- a/lib/core/registry_dynamic.dart +++ b/lib/core/registry_dynamic.dart @@ -3,7 +3,6 @@ library angular.core_dynamic; import 'dart:mirrors'; import 'package:angular/core/module.dart'; -@NgInjectableService() class DynamicMetadataExtractor implements MetadataExtractor { final _fieldAnnotations = [ reflectType(NgAttr), diff --git a/lib/mock/http_backend.dart b/lib/mock/http_backend.dart index 20a8e97a7..5108b8fb4 100644 --- a/lib/mock/http_backend.dart +++ b/lib/mock/http_backend.dart @@ -1,4 +1,12 @@ -part of angular.mock; +library angular.mock.http_backend; + +import 'dart:async' as dart_async; +import 'dart:convert' show JSON; +import 'dart:html'; + +import 'package:angular/angular.dart'; +import 'package:angular/utils.dart' as utils; + class _MockXhr { var $$method, $$url, $$async, $$reqHeaders, $$respHeaders; diff --git a/lib/mock/module.dart b/lib/mock/module.dart index cf9f5eab4..70ba6452f 100644 --- a/lib/mock/module.dart +++ b/lib/mock/module.dart @@ -2,26 +2,23 @@ library angular.mock; import 'dart:async' as dart_async; import 'dart:collection' show ListBase; -import 'dart:convert' show JSON; import 'dart:html'; import 'dart:js' as js; import 'package:angular/angular.dart'; -import 'package:angular/angular_dynamic.dart'; -import 'package:angular/utils.dart' as utils; import 'package:di/di.dart'; -import 'package:di/dynamic_injector.dart'; import 'package:unittest/mock.dart'; +import 'http_backend.dart'; + +export 'http_backend.dart'; export 'zone.dart'; part 'debug.dart'; part 'exception_handler.dart'; -part 'http_backend.dart'; part 'log.dart'; part 'probe.dart'; part 'test_bed.dart'; part 'mock_window.dart'; -part 'test_injection.dart'; /** * Use in addition to [AngularModule] in your tests. diff --git a/lib/mock/test_injection.dart b/lib/mock/test_injection.dart index bf659bedb..37fb13255 100644 --- a/lib/mock/test_injection.dart +++ b/lib/mock/test_injection.dart @@ -1,4 +1,9 @@ -part of angular.mock; +library angular.mock.test_injection; + +import 'package:angular/angular_dynamic.dart'; +import 'package:angular/mock/module.dart'; +import 'package:di/di.dart'; +import 'package:di/dynamic_injector.dart'; _SpecInjector _currentSpecInjector = null; diff --git a/lib/playback/playback_http.dart b/lib/playback/playback_http.dart index f6253a3d9..8b3d6afaa 100644 --- a/lib/playback/playback_http.dart +++ b/lib/playback/playback_http.dart @@ -6,7 +6,7 @@ import 'dart:html'; import 'package:angular/core_dom/module.dart'; import 'package:angular/core/service.dart'; -import 'package:angular/mock/module.dart' as mock; +import 'package:angular/mock/http_backend.dart' as mock; import 'package:angular/playback/playback_data.dart' as playback_data; diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart index 924526ee9..94f94a9d4 100644 --- a/lib/tools/transformer/expression_generator.dart +++ b/lib/tools/transformer/expression_generator.dart @@ -3,7 +3,6 @@ library angular.tools.transformer.expression_generator; import 'dart:async'; import 'dart:math' as math; import 'package:analyzer/src/generated/element.dart'; -import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; import 'package:angular/tools/html_extractor.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; @@ -14,12 +13,8 @@ import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; -import 'package:di/transformer/refactor.dart'; import 'package:path/path.dart' as path; - -const String _generatedExpressionFilename = 'generated_static_expressions.dart'; - /** * Transformer which gathers all expressions from the HTML source files and * Dart source files of an application and packages them for static evaluation. @@ -28,7 +23,7 @@ const String _generatedExpressionFilename = 'generated_static_expressions.dart'; * expressions and modify all references to NG_EXPRESSION_MODULE to refer to * the generated expressions. */ -class ExpressionGenerator extends ResolverTransformer { +class ExpressionGenerator extends Transformer with ResolverTransformer { final TransformOptions options; ExpressionGenerator(this.options, Resolvers resolvers) { @@ -61,16 +56,15 @@ class ExpressionGenerator extends ResolverTransformer { injector.get(_ParserGetterSetter).generateParser( htmlExtractor.expressions.toList(), outputBuffer); - var outputId = - new AssetId(asset.id.package, 'lib/$_generatedExpressionFilename'); + var id = transform.primaryInput.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_expressions.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); transform.addOutput( new Asset.fromString(outputId, outputBuffer.toString())); - transformIdentifiers(transform, resolver, - identifier: 'angular.auto_modules.defaultExpressionModule', - replacement: 'expressionModule', - importPrefix: 'generated_static_expressions', - importUrl: _generatedExpressionFilename); + transform.addOutput(asset); }); } @@ -143,28 +137,26 @@ class _ParserGetterSetter { Map> calls) { return ''' class StaticClosureMap extends ClosureMap { - Map _getters = ${generateGetterMap(properties)}; - Map _setters = ${generateSetterMap(properties)}; - List> _functions = ${generateFunctionMap(calls)}; - - Getter lookupGetter(String name) - => _getters[name]; - Setter lookupSetter(String name) - => _setters[name]; + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; lookupFunction(String name, int arity) - => (arity < _functions.length) ? _functions[arity][name] : null; + => (arity < functions.length) ? functions[arity][name] : null; } + +final Map getters = ${generateGetterMap(properties)}; +final Map setters = ${generateSetterMap(properties)}; +final List> functions = ${generateFunctionMap(calls)}; '''; } generateGetterMap(Iterable keys) { var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); - return '{\n ${lines.join(",\n ")}\n }'; + return '{\n ${lines.join(",\n ")}\n}'; } generateSetterMap(Iterable keys) { var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); - return '{\n ${lines.join(",\n ")}\n }'; + return '{\n ${lines.join(",\n ")}\n}'; } generateFunctionMap(Map> calls) { diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart index fefcf25e4..83cd56dd3 100644 --- a/lib/tools/transformer/metadata_extractor.dart +++ b/lib/tools/transformer/metadata_extractor.dart @@ -2,6 +2,8 @@ library angular.metadata_extractor; import 'package:analyzer/src/generated/ast.dart'; import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/scanner.dart'; +import 'package:analyzer/src/generated/utilities_dart.dart' show ParameterKind; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; @@ -9,8 +11,6 @@ class AnnotatedType { final ClassElement type; Iterable annotations; - final Map members = {}; - AnnotatedType(this.type); /** @@ -24,9 +24,6 @@ class AnnotatedType { for (var annotation in annotations) { annotation.accept(libCollector); } - for (var annotation in members.values) { - annotation.accept(libCollector); - } libs.addAll(libCollector.libraries); return libs; @@ -34,7 +31,7 @@ class AnnotatedType { void writeClassAnnotations(StringBuffer sink, TransformLogger logger, Resolver resolver, Map prefixes) { - sink.write(' ${prefixes[type.library]}${type.name}: [\n'); + sink.write(' ${prefixes[type.library]}${type.name}: const [\n'); var writer = new _AnnotationWriter(sink, prefixes); for (var annotation in annotations) { sink.write(' '); @@ -49,27 +46,6 @@ class AnnotatedType { } sink.write(' ],\n'); } - - void writeMemberAnnotations(StringBuffer sink, TransformLogger logger, - Resolver resolver, Map prefixes) { - if (members.isEmpty) return; - - sink.write(' ${prefixes[type.library]}${type.name}: {\n'); - - var writer = new _AnnotationWriter(sink, prefixes); - members.forEach((memberName, annotation) { - sink.write(' \'$memberName\': '); - if (writer.writeAnnotation(annotation)) { - sink.write(',\n'); - } else { - sink.write('null,\n'); - logger.warning('Unable to serialize annotation $annotation.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); - } - }); - sink.write(' },\n'); - } } /** @@ -231,6 +207,7 @@ class _AnnotationWriter { class AnnotationExtractor { final TransformLogger logger; final Resolver resolver; + final AssetId outputId; static const List _angularAnnotationNames = const [ 'angular.core.NgAttr', @@ -240,10 +217,20 @@ class AnnotationExtractor { 'angular.core.NgCallback' ]; + static const Map _annotationToMapping = const { + 'NgAttr': '@', + 'NgOneWay': '=>', + 'NgOneWayOneTime': '=>!', + 'NgTwoWay': '<=>', + 'NgCallback': '&', + }; + + ClassElement ngAnnotationType; + /// Resolved annotations that this will pick up for members. final List _annotationElements = []; - AnnotationExtractor(this.logger, this.resolver) { + AnnotationExtractor(this.logger, this.resolver, this.outputId) { for (var annotation in _angularAnnotationNames) { var type = resolver.getType(annotation); if (type == null) { @@ -252,14 +239,17 @@ class AnnotationExtractor { } _annotationElements.add(type.unnamedConstructor); } + ngAnnotationType = resolver.getType('angular.core.NgAnnotation'); + if (ngAnnotationType == null) { + logger.warning('Unable to resolve NgAnnotation, ' + 'skipping member annotations.'); + } } AnnotatedType extractAnnotations(ClassElement cls) { - if (resolver.getImportUri(cls.library) == null) { - logger.warning('Dropping annotations for ${cls.name} because the ' - 'containing file cannot be imported (must be in a lib folder).', - asset: resolver.getSourceAssetId(cls), - span: resolver.getSourceSpan(cls)); + if (resolver.getImportUri(cls.library, from: outputId) == null) { + warn('Dropping annotations for ${cls.name} because the ' + 'containing file cannot be imported (must be in a lib folder).', cls); return null; } @@ -273,37 +263,132 @@ class AnnotationExtractor { .where((annotation) { var element = annotation.element; if (element != null && !element.isPublic) { - logger.warning('Annotation $annotation is not public.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); + warn('Annotation $annotation is not public.', + annotation.parent.element); return false; } if (element is ConstructorElement && !element.enclosingElement.isPublic) { - logger.warning('Annotation $annotation is not public.', - asset: resolver.getSourceAssetId(annotation.parent.element), - span: resolver.getSourceSpan(annotation.parent.element)); + warn('Annotation $annotation is not public.', + annotation.parent.element); return false; } return true; }).toList(); + var memberAnnotations = {}; visitor.memberAnnotations.forEach((memberName, annotations) { if (annotations.length > 1) { - logger.warning('$memberName can only have one annotation.', - asset: resolver.getSourceAssetId(annotations[0].parent.element), - span: resolver.getSourceSpan(annotations[0].parent.element)); + warn('$memberName can only have one annotation.', + annotations[0].parent.element); return; } - type.members[memberName] = annotations[0]; + memberAnnotations[memberName] = annotations[0]; }); - if (type.annotations.isEmpty && type.members.isEmpty) return null; + if (memberAnnotations.isNotEmpty) { + _foldMemberAnnotations(memberAnnotations, type); + } + + if (type.annotations.isEmpty) return null; return type; } + + /// Folds all AttrFieldAnnotations into the NgAnnotation annotation on the + /// class. + _foldMemberAnnotations(Map memberAnnotations, + AnnotatedType type) { + // Filter down to NgAnnotation constructors. + var ngAnnotations = type.annotations.where((a) { + var element = a.element; + if (element is! ConstructorElement) return false; + return element.enclosingElement.type.isAssignableTo( + ngAnnotationType.type); + }); + if (ngAnnotations.isEmpty) { + warn('Found field annotation but no class directives.', type.type); + return; + } + + var mapType = resolver.getType('dart.core.Map').type; + // Find acceptable constructors- ones which take a param named 'map' + var acceptableAnnotations = ngAnnotations.where((a) { + var ctor = a.element; + + for (var param in ctor.parameters) { + if (param.parameterKind != ParameterKind.NAMED) { + continue; + } + if (param.name == 'map' && param.type.isAssignableTo(mapType)) { + return true; + } + } + return false; + }); + + if (acceptableAnnotations.isEmpty) { + warn('Could not find a constructor for member annotations in ' + '$ngAnnotations', type.type); + return; + } + + // Default to using the first acceptable annotation- not sure if + // more than one should ever occur. + var annotation = acceptableAnnotations.first; + var mapArg = annotation.arguments.arguments.firstWhere((arg) => + (arg is NamedExpression) && (arg.name.label.name == 'map'), + orElse: () => null); + + // If we don't have a 'map' parameter yet, add one. + if (mapArg == null) { + var map = new MapLiteral(null, null, null, [], null); + var label = new Label(new SimpleIdentifier(stringToken('map')), + new _GeneratedToken(TokenType.COLON, ':')); + mapArg = new NamedExpression(label, map); + annotation.arguments.arguments.add(mapArg); + } + + var map = mapArg.expression; + if (map is! MapLiteral) { + warn('Expected \'map\' argument of $annotation to be a map literal', + type.type); + return; + } + memberAnnotations.forEach((memberName, annotation) { + var key = annotation.arguments.arguments.first; + // If the key already exists then it means we have two annotations for + // same member. + if (map.entries.any((entry) => entry.key.toString() == key.toString())) { + warn('Directive $annotation already contains an entry for $key', + type.type); + return; + } + + var typeName = annotation.element.enclosingElement.name; + var value = '${_annotationToMapping[typeName]}$memberName'; + var entry = new MapLiteralEntry( + key, + new _GeneratedToken(TokenType.COLON, ':'), + new SimpleStringLiteral(stringToken(value), value)); + map.entries.add(entry); + }); + } + + Token stringToken(String str) => new _GeneratedToken(TokenType.STRING, str); + + void warn(String msg, Element element) { + logger.warning(msg, asset: resolver.getSourceAssetId(element), + span: resolver.getSourceSpan(element)); + } +} + +/// Subclass for tokens which we're generating here. +class _GeneratedToken extends Token { + final String lexeme; + _GeneratedToken(TokenType type, this.lexeme) : super(type, 0); } diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart index ca0fb419a..0768cf662 100644 --- a/lib/tools/transformer/metadata_generator.dart +++ b/lib/tools/transformer/metadata_generator.dart @@ -10,9 +10,7 @@ import 'package:path/path.dart' as path; import 'metadata_extractor.dart'; -const String _generatedMetadataFilename = 'generated_metadata.dart'; - -class MetadataGenerator extends ResolverTransformer { +class MetadataGenerator extends Transformer with ResolverTransformer { final TransformOptions options; MetadataGenerator(this.options, Resolvers resolvers) { @@ -24,7 +22,14 @@ class MetadataGenerator extends ResolverTransformer { void applyResolver(Transform transform, Resolver resolver) { var asset = transform.primaryInput; - var extractor = new AnnotationExtractor(transform.logger, resolver); + var id = asset.id; + var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' + '_static_metadata.dart'; + var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); + var outputId = new AssetId(id.package, outputPath); + + var extractor = new AnnotationExtractor(transform.logger, resolver, + outputId); var outputBuffer = new StringBuffer(); _writeHeader(asset.id, outputBuffer); @@ -48,7 +53,7 @@ class MetadataGenerator extends ResolverTransformer { } var prefix = 'import_${index++}'; - var url = resolver.getImportUri(lib); + var url = resolver.getImportUri(lib, from: outputId); outputBuffer.write('import \'$url\' as $prefix;\n'); importPrefixes[lib] = '$prefix.'; } @@ -62,23 +67,9 @@ class MetadataGenerator extends ResolverTransformer { } _writeClassEpilogue(outputBuffer); - _writeMemberPreamble(outputBuffer); - for (var type in annotatedTypes) { - type.writeMemberAnnotations( - outputBuffer, transform.logger, resolver, importPrefixes); - } - _writeMemberEpilogue(outputBuffer); - - var outputId = - new AssetId(asset.id.package, 'lib/$_generatedMetadataFilename'); - transform.addOutput( - new Asset.fromString(outputId, outputBuffer.toString())); - - transformIdentifiers(transform, resolver, - identifier: 'angular.auto_modules.defaultMetadataModule', - replacement: 'metadataModule', - importPrefix: 'generated_metadata', - importUrl: _generatedMetadataFilename); + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + transform.addOutput(asset); } } @@ -87,7 +78,7 @@ void _writeHeader(AssetId id, StringSink sink) { sink.write(''' library ${id.package}.$libPath.generated_metadata; -import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:angular/angular.dart' show MetadataExtractor; import 'package:di/di.dart' show Module; '''); @@ -96,12 +87,11 @@ import 'package:di/di.dart' show Module; void _writePreamble(StringSink sink) { sink.write(''' Module get metadataModule => new Module() - ..value(MetadataExtractor, new _StaticMetadataExtractor()) - ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + ..value(MetadataExtractor, new _StaticMetadataExtractor()); class _StaticMetadataExtractor implements MetadataExtractor { Iterable call(Type type) { - var annotations = _classAnnotations[type]; + var annotations = typeAnnotations[type]; if (annotations != null) { return annotations; } @@ -109,22 +99,12 @@ class _StaticMetadataExtractor implements MetadataExtractor { } } -class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { - Map call(Type type) { - var annotations = _memberAnnotations[type]; - if (annotations != null) { - return annotations; - } - return {}; - } -} - '''); } void _writeClassPreamble(StringSink sink) { sink.write(''' -final Map _classAnnotations = { +final Map typeAnnotations = { '''); } @@ -134,19 +114,6 @@ void _writeClassEpilogue(StringSink sink) { '''); } -void _writeMemberPreamble(StringSink sink) { - sink.write(''' - -final Map> _memberAnnotations = { -'''); -} - -void _writeMemberEpilogue(StringSink sink) { - sink.write(''' -}; -'''); -} - void _writeFooter(StringSink sink) { sink.write(''' }; diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart index 3fe347db6..ce681013c 100644 --- a/lib/tools/transformer/options.dart +++ b/lib/tools/transformer/options.dart @@ -10,11 +10,11 @@ import 'package:path/path.dart' as path; class TransformOptions { /** - * The file path of the primary Dart entry point (main) for the application. + * The file paths of the primary Dart entry point (main) for the application. * This is used as the starting point to find all expressions used by the * application. */ - final String dartEntry; + final Set dartEntries; /** * List of html file paths which may contain Angular expressions. @@ -38,11 +38,11 @@ class TransformOptions { */ final di.TransformOptions diOptions; - TransformOptions({String dartEntry, + TransformOptions({List dartEntries, String sdkDirectory, List htmlFiles, Map templateUriRewrites, di.TransformOptions diOptions}) - : dartEntry = dartEntry, + : dartEntries = dartEntries.toSet(), sdkDirectory = sdkDirectory, htmlFiles = htmlFiles != null ? htmlFiles : [], templateUriRewrites = templateUriRewrites != null ? @@ -54,5 +54,5 @@ class TransformOptions { // Don't need to check package as transformers only run for primary package. Future isDartEntry(AssetId id) => - new Future.value(id.path == dartEntry || dartEntry == '*'); + new Future.value(dartEntries.contains(id.path)); } diff --git a/lib/tools/transformer/static_angular_generator.dart b/lib/tools/transformer/static_angular_generator.dart new file mode 100644 index 000000000..5b0132571 --- /dev/null +++ b/lib/tools/transformer/static_angular_generator.dart @@ -0,0 +1,101 @@ +library angular.tools.transformer.static_angular_generator; + +import 'dart:async'; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:di/transformer/refactor.dart'; +import 'package:barback/barback.dart'; +import 'package:path/path.dart' as path; +import 'package:source_maps/refactor.dart' show TextEditTransaction; + +class StaticAngularGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + StaticAngularGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => new Future.value( + options.isDartEntry(input.id)); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + + var dynamicApp = resolver.getType('angular.dynamic.NgDynamicApp'); + if (dynamicApp == null) { + // No dynamic app imports, exit. + transform.addOutput(transform.primaryInput); + return; + } + + var id = asset.id; + var lib = resolver.entryLibrary; + var transaction = resolver.createTextEditTransaction(lib); + var unit = lib.definingCompilationUnit.node; + + for (var directive in unit.directives) { + if (directive is ImportDirective && + directive.uri.stringValue == 'package:angular/angular_dynamic.dart') { + var uri = directive.uri; + transaction.edit(uri.beginToken.offset, uri.end, + '\'package:angular/angular_static.dart\''); + } + } + + var dynamicToStatic = new _NgDynamicToStaticVisitor( + dynamicApp.unnamedConstructor, transaction); + unit.accept(dynamicToStatic); + + var generatedFilePrefix = '${path.url.basenameWithoutExtension(id.path)}'; + _addImport(transaction, unit, + '${generatedFilePrefix}_static_expressions.dart', + 'generated_static_expressions'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_metadata.dart', + 'generated_static_metadata'); + _addImport(transaction, unit, + '${generatedFilePrefix}_static_injector.dart', + 'generated_static_injector'); + + var printer = transaction.commit(); + var url = id.path.startsWith('lib/') + ? 'package:${id.package}/${id.path.substring(4)}' : id.path; + printer.build(url); + transform.addOutput(new Asset.fromString(id, printer.text)); + } +} + +/// Injects an import into the list of imports in the file. +void _addImport(TextEditTransaction transaction, CompilationUnit unit, + String uri, String prefix) { + var last = unit.directives.where((d) => d is ImportDirective).last; + transaction.edit(last.end, last.end, '\nimport \'$uri\' as $prefix;'); +} + +class _NgDynamicToStaticVisitor extends GeneralizingASTVisitor { + final ConstructorElement constructor; + final TextEditTransaction transaction; + _NgDynamicToStaticVisitor(this.constructor, this.transaction); + + visitInstanceCreationExpression(InstanceCreationExpression node) { + if (node.staticElement == constructor) { + var ctorName = node.constructorName; + var ctorStr = ctorName.toString(); + var prefix = ctorStr.indexOf('.') == -1 ? '' : + ctorStr.substring(0, ctorStr.indexOf('.') + 1); + + transaction.edit(ctorName.beginToken.offset, ctorName.end, + '${prefix}NgStaticApp'); + + var args = node.argumentList; + transaction.edit(args.beginToken.offset + 1, args.end - 1, + 'generated_static_injector.factories, ' + 'generated_static_metadata.typeAnnotations, ' + 'generated_static_expressions.getters, ' + 'new generated_static_expressions.StaticClosureMap()'); + } + return super.visitNode(node); + } +} diff --git a/lib/transformer.dart b/lib/transformer.dart index 977c8cbb5..43a2648ed 100644 --- a/lib/transformer.dart +++ b/lib/transformer.dart @@ -3,6 +3,7 @@ library angular.transformer; import 'dart:io'; import 'package:angular/tools/transformer/expression_generator.dart'; import 'package:angular/tools/transformer/metadata_generator.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; import 'package:angular/tools/transformer/options.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; @@ -38,8 +39,11 @@ TransformOptions _parseSettings(Map args) { 'angular.core.NgFilter']; annotations.addAll(_readStringListValue(args, 'injectable_annotations')); - var injectedTypes = ['perf_api.Profiler', - 'angular.core.parser.static_parser.StaticParser']; + var injectedTypes = [ + 'perf_api.Profiler', + 'angular.core.RootScope', + 'angular.core.AstParser', + 'angular.core.dom.NgAnimate']; injectedTypes.addAll(_readStringListValue(args, 'injected_types')); var sdkDir = _readStringValue(args, 'dart_sdk', required: false); @@ -48,16 +52,16 @@ TransformOptions _parseSettings(Map args) { sdkDir = path.dirname(path.dirname(Platform.executable)); } - var dartEntry = _readStringValue(args, 'dart_entry'); + var dartEntries = _readStringListValue(args, 'dart_entries'); var diOptions = new di.TransformOptions( - dartEntries: [dartEntry], + dartEntries: dartEntries, injectableAnnotations: annotations, injectedTypes: injectedTypes, sdkDirectory: sdkDir); return new TransformOptions( - dartEntry: dartEntry, + dartEntries: dartEntries, htmlFiles: _readStringListValue(args, 'html_files'), sdkDirectory: sdkDir, templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), @@ -125,5 +129,6 @@ List> _createPhases(TransformOptions options) { [new ExpressionGenerator(options, resolvers)], [new di.InjectorGenerator(options.diOptions, resolvers)], [new MetadataGenerator(options, resolvers)], + [new StaticAngularGenerator(options, resolvers)], ]; } diff --git a/test/_specs.dart b/test/_specs.dart index 093915edb..cbfa9ab4b 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -4,6 +4,7 @@ import 'dart:html'; import 'package:unittest/unittest.dart' as unit; import 'package:angular/angular.dart'; import 'package:angular/mock/module.dart'; +import 'package:angular/mock/test_injection.dart'; import 'package:collection/wrappers.dart' show DelegatingList; import 'jasmine_syntax.dart' as jasmine_syntax; @@ -15,6 +16,7 @@ export 'package:di/dynamic_injector.dart'; export 'package:angular/angular.dart'; export 'package:angular/animate/module.dart'; export 'package:angular/mock/module.dart'; +export 'package:angular/mock/test_injection.dart'; export 'package:perf_api/perf_api.dart'; es(String html) { diff --git a/test/jasmine_syntax.dart b/test/jasmine_syntax.dart index d50b8fb62..fb2296832 100644 --- a/test/jasmine_syntax.dart +++ b/test/jasmine_syntax.dart @@ -8,7 +8,7 @@ _withSetup(fn) => () { _beforeEachFnsForCurrentTest.sort((a, b) => Comparable.compare(b[1], a[1])); _beforeEachFnsForCurrentTest.forEach((fn) => fn[0]()); try { - fn(); + return fn(); } finally { _beforeEachFnsForCurrentTest = []; } diff --git a/test/tools/transformer/all.dart b/test/tools/transformer/all.dart new file mode 100644 index 000000000..fa81732f4 --- /dev/null +++ b/test/tools/transformer/all.dart @@ -0,0 +1,11 @@ +library all_transformer_tests; + +import 'expression_generator_spec.dart' as expression_generator_spec; +import 'metadata_generator_spec.dart' as metadata_generator_spec; +import 'static_angular_generator_spec.dart' as static_angular_generator_spec; + +main() { + expression_generator_spec.main(); + metadata_generator_spec.main(); + static_angular_generator_spec.main(); +} diff --git a/test/tools/transformer/expression_extractor_spec.dart b/test/tools/transformer/expression_generator_spec.dart similarity index 57% rename from test/tools/transformer/expression_extractor_spec.dart rename to test/tools/transformer/expression_generator_spec.dart index de7a68f9e..d36a15426 100644 --- a/test/tools/transformer/expression_extractor_spec.dart +++ b/test/tools/transformer/expression_generator_spec.dart @@ -7,10 +7,10 @@ import 'package:code_transformers/tests.dart' as tests; import '../../jasmine_syntax.dart'; main() { - describe('expression_extractor', () { + describe('ExpressionGenerator', () { var htmlFiles = []; var options = new TransformOptions( - dartEntry: 'web/main.dart', + dartEntries: ['web/main.dart'], htmlFiles: htmlFiles, sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -23,37 +23,34 @@ main() { htmlFiles.add('web/index.html'); return tests.applyTransformers(phases, inputs: { - 'angular|lib/auto_modules.dart': PACKAGE_AUTO, 'a|web/main.dart': ''' library foo; -import 'package:angular/auto_modules.dart'; ''', 'a|web/index.html': '''
{{some.getter}}
''' }, results: { - 'a|lib/generated_static_expressions.dart': ''' -$HEADER - Map _getters = { - r"some": (o) => o.some, - r"getter": (o) => o.getter - }; - Map _setters = { - r"some": (o, v) => o.some = v, - r"getter": (o, v) => o.getter = v - }; - List> _functions = []; -$FOOTER + 'a|web/main_static_expressions.dart': ''' +$header +final Map getters = { + r"some": (o) => o.some, + r"getter": (o) => o.getter +}; +final Map setters = { + r"some": (o, v) => o.some = v, + r"getter": (o, v) => o.getter = v +}; +final List> functions = []; ''' - }).then((_) { + }).whenComplete(() { htmlFiles.clear(); }); }); }); } -const String HEADER = ''' +const String header = ''' library a.web.main.generated_expressions; import 'package:angular/angular.dart'; @@ -62,20 +59,10 @@ import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; Module get expressionModule => new Module() ..value(ClosureMap, new StaticClosureMap()); -class StaticClosureMap extends ClosureMap {'''; - -const String FOOTER = ''' - - Getter lookupGetter(String name) - => _getters[name]; - Setter lookupSetter(String name) - => _setters[name]; +class StaticClosureMap extends ClosureMap { + Getter lookupGetter(String name) => getters[name]; + Setter lookupSetter(String name) => setters[name]; lookupFunction(String name, int arity) - => (arity < _functions.length) ? _functions[arity][name] : null; -}'''; - -const String PACKAGE_AUTO = ''' -library angular.auto_modules; - -Module get defaultExpressionModule => new Module(); + => (arity < functions.length) ? functions[arity][name] : null; +} '''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart index 69528c504..61e9ebe07 100644 --- a/test/tools/transformer/metadata_generator_spec.dart +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -11,9 +11,9 @@ import 'package:code_transformers/tests.dart' as tests; import '../../jasmine_syntax.dart'; main() { - describe('metadata_generator', () { + describe('MetadataGenerator', () { var options = new TransformOptions( - dartEntry: 'web/main.dart', + dartEntries: ['web/main.dart'], sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -25,9 +25,8 @@ main() { it('should extract member metadata', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(selector: r'[*=/{{.*}}/]') @@ -46,66 +45,104 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { 'import_0.Engine': [ - 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\')', + 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\', map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'callback\': \'&callback\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'' + '})', 'proxy', ] - }, - classMembers: { - 'import_0.Engine': { - 'anotherExpression': 'const import_1.NgOneWay(\'another-expression\')', - 'callback': 'const import_1.NgCallback(\'callback\')', - 'twoWayStuff': 'const import_1.NgTwoWay(\'two-way-stuff\')', - } }); }); - it('should warn on un-importable files', () { + it('should warn on multiple annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|web/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; - @NgDirective(selector: r'[*=/{{.*}}/]') - class Engine {} + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } ''' }, - messages: ['warning: Dropping annotations for Engine because the ' - 'containing file cannot be imported (must be in a lib folder). ' - '(a.dart 2 16)']); + messages: ['warning: callback can only have one annotation. ' + '(main.dart 3 18)']); }); - it('should warn on multiple annotations', () { + it('should warn on duplicated annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; + @NgDirective(map: {'another-expression': '=>anotherExpression'}) class Engine { - @NgCallback('callback') @NgOneWay('another-expression') - set callback(Function) {} + set anotherExpression(Function) {} } ''' }, - messages: ['warning: callback can only have one annotation. ' - '(package:a/a.dart 3 18)']); + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\'})', + ] + }, + messages: ['warning: Directive @NgOneWay(\'another-expression\') ' + 'already contains an entry for \'another-expression\' ' + '(main.dart 2 16)' + ]); + }); + + it('should merge member annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: {'another-expression': '=>anotherExpression'}) + class Engine { + set anotherExpression(Function) {} + + set twoWayStuff(String abc) {} + @NgTwoWay('two-way-stuff') + String get twoWayStuff => null; + } + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'})', + ] + }); }); it('should warn on multiple annotations (across getter/setter)', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; class Engine { @@ -118,15 +155,14 @@ main() { ''' }, messages: ['warning: callback can only have one annotation. ' - '(package:a/a.dart 3 18)']); + '(main.dart 3 18)']); }); it('should extract map arguments', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) @@ -134,7 +170,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -147,9 +183,8 @@ main() { it('should extract list arguments', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(publishTypes: const [TextChangeListener]) @@ -157,7 +192,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -170,9 +205,8 @@ main() { it('should extract primitive literals', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgOneWay(true) @@ -183,7 +217,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -199,9 +233,8 @@ main() { it('should skip and warn on unserializable annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @Foo @@ -212,7 +245,7 @@ main() { ''' }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -224,20 +257,17 @@ main() { ] }, messages: [ - 'warning: Unable to serialize annotation @Foo. ' - '(package:a/a.dart 2 16)', + 'warning: Unable to serialize annotation @Foo. (main.dart 2 16)', 'warning: Unable to serialize annotation ' - '@NgDirective(publishTypes: const [Foo]). ' - '(package:a/a.dart 5 16)', + '@NgDirective(publishTypes: const [Foo]). (main.dart 5 16)', ]); }); it('should extract types across libs', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; import 'package:a/b.dart'; @@ -249,7 +279,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', 'import \'package:a/b.dart\' as import_2;', ], @@ -263,9 +293,8 @@ main() { it('should not gather non-member annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; class Engine { @@ -281,9 +310,8 @@ main() { it('properly escapes strings', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': r''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': r''' import 'package:angular/angular.dart'; @NgOneWay('foo\' \\') @@ -292,7 +320,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -305,9 +333,8 @@ main() { it('should reference static and global properties', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) @@ -318,7 +345,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', 'import \'package:angular/angular.dart\' as import_1;', ], classes: { @@ -332,9 +359,8 @@ main() { it('should not extract private annotations', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @_Foo() @@ -349,18 +375,16 @@ main() { ''', }, messages: [ - 'warning: Annotation @_Foo() is not public. ' - '(package:a/a.dart 2 16)', - 'warning: Annotation @_foo is not public. (package:a/a.dart 2 16)', + 'warning: Annotation @_Foo() is not public. (main.dart 2 16)', + 'warning: Annotation @_foo is not public. (main.dart 2 16)', ]); }); it('supports named constructors', () { return generates(phases, inputs: { - 'a|web/main.dart': '''import 'package:a/a.dart'; ''', - 'angular|lib/angular.dart': PACKAGE_ANGULAR, - 'a|lib/a.dart': ''' + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' import 'package:angular/angular.dart'; @Foo.bar() @@ -375,7 +399,7 @@ main() { ''', }, imports: [ - 'import \'package:a/a.dart\' as import_0;', + 'import \'main.dart\' as import_0;', ], classes: { 'import_0.Engine': [ @@ -384,7 +408,7 @@ main() { }, messages: [ 'warning: Annotation @Foo._private() is not public. ' - '(package:a/a.dart 2 16)', + '(main.dart 2 16)', ]); }); }); @@ -393,57 +417,46 @@ main() { Future generates(List> phases, {Map inputs, Iterable imports: const [], Map classes: const {}, - Map classMembers: const {}, Iterable messages: const []}) { var buffer = new StringBuffer(); - buffer.write('$HEADER\n'); + buffer.write('$header\n'); for (var i in imports) { buffer.write('$i\n'); } - buffer.write('$BOILER_PLATE\n'); + buffer.write('$boilerPlate\n'); for (var className in classes.keys) { - buffer.write(' $className: [\n'); + buffer.write(' $className: const [\n'); for (var annotation in classes[className]) { buffer.write(' $annotation,\n'); } buffer.write(' ],\n'); } - buffer.write('$MEMBER_PREAMBLE\n'); - for (var className in classMembers.keys) { - buffer.write(' $className: {\n'); - var members = classMembers[className]; - for (var memberName in members.keys) { - buffer.write(' \'$memberName\': ${members[memberName]},\n'); - } - buffer.write(' },\n'); - } - buffer.write('$FOOTER\n'); + buffer.write('$footer\n'); return tests.applyTransformers(phases, inputs: inputs, results: { - 'a|lib/generated_metadata.dart': buffer.toString() + 'a|web/main_static_metadata.dart': buffer.toString() }, messages: messages); } -const String HEADER = ''' +const String header = ''' library a.web.main.generated_metadata; -import 'package:angular/angular.dart' show AttrFieldAnnotation, FieldMetadataExtractor, MetadataExtractor; +import 'package:angular/angular.dart' show MetadataExtractor; import 'package:di/di.dart' show Module; '''; -const String BOILER_PLATE = ''' +const String boilerPlate = ''' Module get metadataModule => new Module() - ..value(MetadataExtractor, new _StaticMetadataExtractor()) - ..value(FieldMetadataExtractor, new _StaticFieldMetadataExtractor()); + ..value(MetadataExtractor, new _StaticMetadataExtractor()); class _StaticMetadataExtractor implements MetadataExtractor { Iterable call(Type type) { - var annotations = _classAnnotations[type]; + var annotations = typeAnnotations[type]; if (annotations != null) { return annotations; } @@ -451,32 +464,21 @@ class _StaticMetadataExtractor implements MetadataExtractor { } } -class _StaticFieldMetadataExtractor implements FieldMetadataExtractor { - Map call(Type type) { - var annotations = _memberAnnotations[type]; - if (annotations != null) { - return annotations; - } - return {}; - } -} - -final Map _classAnnotations = {'''; - -const String MEMBER_PREAMBLE = ''' -}; +final Map typeAnnotations = {'''; -final Map> _memberAnnotations = {'''; - -const String FOOTER = ''' +const String footer = ''' };'''; -const String PACKAGE_ANGULAR = ''' +const String libAngular = ''' library angular.core; -class NgDirective { - const NgDirective({selector, publishTypes, map, visibility}); +class NgAnnotation { + NgAnnotation({map: const {}}); +} + +class NgDirective extends NgAnnotation { + const NgDirective({selector, publishTypes, map, visibility}) : super(map: map); static const int CHILDREN_VISIBILITY = 1; } diff --git a/test/tools/transformer/static_angular_generator_spec.dart b/test/tools/transformer/static_angular_generator_spec.dart new file mode 100644 index 000000000..c9ece2c3a --- /dev/null +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -0,0 +1,111 @@ +library angular.test.tools.transformer.metadata_generator_spec; + +import 'dart:async'; + +import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/static_angular_generator.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('StaticAngularGenerator', () { + var options = new TransformOptions( + dartEntries: ['web/main.dart'], + sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new StaticAngularGenerator(options, resolvers)] + ]; + + it('should modify NgDynamicApp', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart'; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart'; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + + it('handles prefixed app imports', () { + return tests.applyTransformers(phases, + inputs: { + 'angular|lib/angular_dynamic.dart': libAngularDynamic, + 'di|lib/di.dart': libDI, + 'a|web/main.dart': ''' +import 'package:angular/angular_dynamic.dart' as ng; +import 'package:di/di.dart' show Module; + +class MyModule extends Module {} + +main() { + var app = new ng.NgDynamicApp() + .addModule(new MyModule()) + .run(); +} +''' + }, + results: { + 'a|web/main.dart': ''' +import 'package:angular/angular_static.dart' as ng; +import 'package:di/di.dart' show Module; +import 'main_static_expressions.dart' as generated_static_expressions; +import 'main_static_metadata.dart' as generated_static_metadata; +import 'main_static_injector.dart' as generated_static_injector; + +class MyModule extends Module {} + +main() { + var app = new ng.NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + }); +} + + + +const String libAngularDynamic = ''' +library angular.dynamic + +class NgDynamicApp {}; +'''; + +const String libDI = ''' +class Module {} +'''; From 25db8c0ef5dd2cc60af85869f42e417ea9445014 Mon Sep 17 00:00:00 2001 From: Pete Blois Date: Mon, 17 Mar 2014 12:54:13 -0700 Subject: [PATCH 3/3] Further cleanup to get all examples working with transformers. Entry points are now automatically detected. Expression extractor is now pulling HTML contents from templateUris in annotations. --- example/pubspec.lock | 17 +- example/pubspec.yaml | 12 +- example/web/animation.html | 2 +- example/web/bouncing_balls.dart | 5 +- example/web/hello_world.dart | 8 - lib/animate/animation_loop.dart | 12 +- lib/animate/animation_optimizer.dart | 1 + lib/animate/css_animate.dart | 2 + lib/change_detection/change_detection.dart | 2 + lib/core/parser/dynamic_parser.dart | 1 - lib/core/registry.dart | 2 - lib/tools/parser_getter_setter/generator.dart | 2 +- .../transformer/expression_generator.dart | 106 ++++--- .../html_dart_references_generator.dart | 50 ++++ lib/tools/transformer/metadata_extractor.dart | 38 +-- lib/tools/transformer/metadata_generator.dart | 3 +- lib/tools/transformer/options.dart | 20 +- lib/tools/transformer/referenced_uris.dart | 259 ++++++++++++++++++ .../transformer/static_angular_generator.dart | 37 ++- lib/transformer.dart | 16 +- pubspec.lock | 12 +- pubspec.yaml | 12 +- .../expression_generator_spec.dart | 139 +++++++--- .../transformer/metadata_generator_spec.dart | 89 ++++-- .../static_angular_generator_spec.dart | 21 +- 25 files changed, 627 insertions(+), 241 deletions(-) create mode 100644 lib/tools/transformer/html_dart_references_generator.dart create mode 100644 lib/tools/transformer/referenced_uris.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index 6d13f3cd4..cd9f21030 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.12.2" + version: "0.13.0-dev.9" angular: description: path: ".." @@ -14,7 +14,7 @@ packages: args: description: args source: hosted - version: "0.9.0" + version: "0.10.0+1" barback: description: barback source: hosted @@ -26,17 +26,16 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.0.1-dev.2" + version: "0.0.1-dev.3" collection: description: collection source: hosted version: "0.9.1" di: description: - ref: null - resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" - url: "https://github.com/angular/di.dart.git" - source: git + path: "../../di.dart" + relative: true + source: path version: "0.0.33" html5lib: description: html5lib @@ -82,10 +81,6 @@ packages: description: unittest source: hosted version: "0.10.0" - unmodifiable_collection: - description: unmodifiable_collection - source: hosted - version: "0.9.2+1" utf: description: utf source: hosted diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3e0e3b133..ca29799df 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,14 +7,4 @@ dependencies: unittest: any transformers: -- angular: - dart_entries: - - web/todo.dart - - web/bouncing_balls.dart - html_files: # Need to split out to per-entry assets - - web/todo.html - - web/bouncing_balls.html - -dependency_overrides: - di: - git: https://github.com/angular/di.dart.git +- angular diff --git a/example/web/animation.html b/example/web/animation.html index 6d03ad628..88e20519b 100644 --- a/example/web/animation.html +++ b/example/web/animation.html @@ -41,6 +41,6 @@

Stress Test

- + diff --git a/example/web/bouncing_balls.dart b/example/web/bouncing_balls.dart index c5bdd3437..b8d64f30b 100644 --- a/example/web/bouncing_balls.dart +++ b/example/web/bouncing_balls.dart @@ -102,7 +102,8 @@ class BounceController { @NgDirective( selector: '[ball-position]', map: const { - "ball-position": '=>position'}) + "ball-position": '=>position'}, + exportExpressions: const ['x', 'y']) class BallPositionDirective { final Element element; final Scope scope; @@ -125,7 +126,7 @@ class MyModule extends Module { } main() { - new NgDynamicApp() + ngDynamicApp() .addModule(new MyModule()) .run(); } diff --git a/example/web/hello_world.dart b/example/web/hello_world.dart index d9444d35e..cda73d9a3 100644 --- a/example/web/hello_world.dart +++ b/example/web/hello_world.dart @@ -1,14 +1,6 @@ import 'package:angular/angular.dart'; import 'package:angular/angular_dynamic.dart'; -// This annotation allows Dart to shake away any classes -// not used from Dart code nor listed in another @MirrorsUsed. -// -// If you create classes that are referenced from the Angular -// expressions, you must include a library target in @MirrorsUsed. -@MirrorsUsed(override: '*') -import 'dart:mirrors'; - @NgController( selector: '[hello-world-controller]', publishAs: 'ctrl') diff --git a/lib/animate/animation_loop.dart b/lib/animate/animation_loop.dart index 0d8a2fc41..a468f6dd7 100644 --- a/lib/animate/animation_loop.dart +++ b/lib/animate/animation_loop.dart @@ -4,6 +4,7 @@ part of angular.animate; * Window.animationFrame update loop that tracks and drives * [LoopedAnimations]'s. */ +@NgInjectableService() class AnimationLoop { final AnimationFrame _frames; final Profiler _profiler; @@ -23,7 +24,7 @@ class AnimationLoop { * Start and play an animation through the state transitions defined in * [Animation]. */ - void play(LoopedAnimation animation) { + void play(LoopedAnimation animation) { _animations.add(animation); _queueAnimationFrame(); } @@ -42,7 +43,7 @@ class AnimationLoop { /* On the browsers animation frame event, update each of the tracked * animations. Group dom reads first, and and writes second. - * + * * At any point any animation may be updated by calling interrupt and cancel * with a reference to the [Animation] to cancel. The [AnimationRunner] will * then forget about the [Animation] and will not call any further methods on @@ -56,7 +57,7 @@ class AnimationLoop { // Dom reads _read(timeInMs); _profiler.stopTimer("AnimationRunner.AnimationFrame.DomReads"); - + _profiler.startTimer("AnimationRunner.AnimationFrame.DomMutates"); // Dom mutates _update(timeInMs); @@ -86,7 +87,7 @@ class AnimationLoop { animation.read(timeInMs); } } - + /** * Stop tracking and updating the [animation]. */ @@ -100,9 +101,10 @@ class AnimationLoop { * Wrapper around window.requestAnimationFrame so it can be intercepted and * tested. */ +@NgInjectableService() class AnimationFrame { final dom.Window _wnd; Future get animationFrame => _wnd.animationFrame; - + AnimationFrame(this._wnd); } diff --git a/lib/animate/animation_optimizer.dart b/lib/animate/animation_optimizer.dart index 0d7e6958b..104f27b5a 100644 --- a/lib/animate/animation_optimizer.dart +++ b/lib/animate/animation_optimizer.dart @@ -6,6 +6,7 @@ part of angular.animate; * running animations on child elements while the dom parent is also running an * animation. */ +@NgInjectableService() class AnimationOptimizer { final Map> _elements = new Map>(); diff --git a/lib/animate/css_animate.dart b/lib/animate/css_animate.dart index 6ac0d541f..43d3fc70e 100644 --- a/lib/animate/css_animate.dart +++ b/lib/animate/css_animate.dart @@ -6,6 +6,7 @@ part of angular.animate; * animation framework. This implementation uses the [AnimationLoop] class to * queue and run CSS based transition and keyframe animations. */ +@NgInjectableService() class CssAnimate implements NgAnimate { static const NG_ANIMATE = "ng-animate"; static const NG_MOVE = "ng-move"; @@ -130,6 +131,7 @@ class CssAnimate implements NgAnimate { /** * Tracked set of currently running css animations grouped by element. */ +@NgInjectableService() class CssAnimationMap { final Map> cssAnimations = new Map>(); diff --git a/lib/change_detection/change_detection.dart b/lib/change_detection/change_detection.dart index de368f258..5542cd535 100644 --- a/lib/change_detection/change_detection.dart +++ b/lib/change_detection/change_detection.dart @@ -232,6 +232,8 @@ abstract class RemovedItem extends CollectionChangeItem { typedef FieldGetter(object); +typedef FieldSetter(object, value); + abstract class FieldGetterFactory { FieldGetter call(Object object, String name); } diff --git a/lib/core/parser/dynamic_parser.dart b/lib/core/parser/dynamic_parser.dart index b749944a9..9112ebe37 100644 --- a/lib/core/parser/dynamic_parser.dart +++ b/lib/core/parser/dynamic_parser.dart @@ -11,7 +11,6 @@ import 'package:angular/core/parser/eval.dart'; import 'package:angular/core/parser/utils.dart' show EvalError; import 'package:angular/utils.dart'; -@NgInjectableService() abstract class ClosureMap { Getter lookupGetter(String name); Setter lookupSetter(String name); diff --git a/lib/core/registry.dart b/lib/core/registry.dart index 8bfad16bf..6e6cffbb7 100644 --- a/lib/core/registry.dart +++ b/lib/core/registry.dart @@ -68,8 +68,6 @@ abstract class AnnotationsMap { } } - -@NgInjectableService() abstract class MetadataExtractor { Iterable call(Type type); } diff --git a/lib/tools/parser_getter_setter/generator.dart b/lib/tools/parser_getter_setter/generator.dart index cd4532cfa..7e4af09b9 100644 --- a/lib/tools/parser_getter_setter/generator.dart +++ b/lib/tools/parser_getter_setter/generator.dart @@ -58,7 +58,7 @@ class StaticClosureMap extends ClosureMap { => _getters[name]; Setter lookupSetter(String name) => _setters[name]; - lookupFunction(String name, int arity) + lookupFunction(String name, int arity) => (arity < _functions.length) ? _functions[arity][name] : null; } '''; diff --git a/lib/tools/transformer/expression_generator.dart b/lib/tools/transformer/expression_generator.dart index 94f94a9d4..0f2961209 100644 --- a/lib/tools/transformer/expression_generator.dart +++ b/lib/tools/transformer/expression_generator.dart @@ -9,6 +9,7 @@ import 'package:angular/tools/parser_getter_setter/generator.dart'; import 'package:angular/tools/source_crawler.dart'; import 'package:angular/tools/source_metadata_extractor.dart'; import 'package:angular/tools/transformer/options.dart'; +import 'package:angular/tools/transformer/referenced_uris.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:di/di.dart'; @@ -30,7 +31,7 @@ class ExpressionGenerator extends Transformer with ResolverTransformer { this.resolvers = resolvers; } - Future isPrimary(Asset input) => options.isDartEntry(input.id); + Future isPrimary(Asset input) => options.isDartEntry(input); Future applyResolver(Transform transform, Resolver resolver) { var asset = transform.primaryInput; @@ -44,7 +45,7 @@ class ExpressionGenerator extends Transformer with ResolverTransformer { new _LibrarySourceCrawler(resolver.libraries)); var htmlExtractor = new HtmlExpressionExtractor(directives); - return _getHtmlSources(transform) + return _getHtmlSources(transform, resolver) .forEach(htmlExtractor.parseHtml) .then((_) { var module = new Module() @@ -72,22 +73,48 @@ class ExpressionGenerator extends Transformer with ResolverTransformer { * Gets a stream consisting of the contents of all HTML source files to be * scoured for expressions. */ - Stream _getHtmlSources(Transform transform) { + Stream _getHtmlSources(Transform transform, Resolver resolver) { + var id = transform.primaryInput.id; + var controller = new StreamController(); - if (options.htmlFiles == null) { - controller.close(); - return controller.stream; - } - Future.wait(options.htmlFiles.map((path) { - var htmlId = new AssetId(transform.primaryInput.id.package, path); - return transform.readInputAsString(htmlId); - }).map((future) { - return future.then(controller.add).catchError(controller.addError); - })).then((_) { - controller.close(); + var assets = options.htmlFiles + .map((path) => new AssetId(id.package, path)) + .toList(); + + // Get all of the contents of templates in @NgComponent(templateUrl:'...') + gatherReferencedUris(transform, resolver, options, + templatesOnly: true).then((templates) { + templates.values.forEach(controller.add); + }).then((_) { + // Add any HTML files referencing this Dart file. + return _findHtmlEntry(transform); + }).then((htmlRefId) { + if (htmlRefId != null) { + assets.add(htmlRefId); + } + Future.wait( + // Add any manually specified HTML files. + assets.map((id) => transform.readInputAsString(id)) + .map((future) => + future.then(controller.add).catchError(controller.addError)) + ).then((_) { + controller.close(); + }); }); + return controller.stream; } + + /// Finds any HTML files referencing the primary input of the transform. + Future _findHtmlEntry(Transform transform) { + var id = transform.primaryInput.id; + // Magic file generated by HtmlDartReferencesGenerator + var htmlRefId = new AssetId(id.package, id.path + '.html_reference'); + + return transform.readInputAsString(htmlRefId).then((path) { + return new AssetId(id.package, path); + }, onError: (e, s) => null); // swallow not-found errors. + } } void _writeStaticExpressionHeader(AssetId id, StringSink sink) { @@ -95,17 +122,13 @@ void _writeStaticExpressionHeader(AssetId id, StringSink sink) { sink.write(''' library ${id.package}.$libPath.generated_expressions; -import 'package:angular/angular.dart'; -import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; - -Module get expressionModule => new Module() - ..value(ClosureMap, new StaticClosureMap()); +import 'package:angular/change_detection/change_detection.dart'; '''); } class _LibrarySourceCrawler implements SourceCrawler { - final List libraries; + final Iterable libraries; _LibrarySourceCrawler(this.libraries); void crawl(String entryPoint, CompilationUnitVisitor visitor) { @@ -130,22 +153,15 @@ class _ParserGetterSetter { }); DartGetterSetterGen backend = this.backend; - sink.write(generateClosureMap(backend.properties, backend.calls)); + sink.write(generateClosures(backend.properties, backend.calls)); } - String generateClosureMap(Set properties, + String generateClosures(Set properties, Map> calls) { + var getters = new Set.from(properties.toList())..addAll(calls.keys); return ''' -class StaticClosureMap extends ClosureMap { - Getter lookupGetter(String name) => getters[name]; - Setter lookupSetter(String name) => setters[name]; - lookupFunction(String name, int arity) - => (arity < functions.length) ? functions[arity][name] : null; -} - -final Map getters = ${generateGetterMap(properties)}; -final Map setters = ${generateSetterMap(properties)}; -final List> functions = ${generateFunctionMap(calls)}; +final Map getters = ${generateGetterMap(getters)}; +final Map setters = ${generateSetterMap(properties)}; '''; } @@ -158,31 +174,5 @@ final List> functions = ${generateFunctionMap(calls)}; var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); return '{\n ${lines.join(",\n ")}\n}'; } - - generateFunctionMap(Map> calls) { - Map> arities = {}; - calls.forEach((name, callArities) { - callArities.forEach((arity){ - arities.putIfAbsent(arity, () => new Set()).add(name); - }); - }); - - var maxArity = arities.isEmpty ? 0 : - arities.keys.reduce((x, y) => math.max(x, y)); - - var maps = new Iterable.generate(maxArity, (arity) { - var names = arities[arity]; - if (names == null) { - return '{\n }'; - } else { - var args = new List.generate(arity, (e) => "a$e").join(','); - var p = args.isEmpty ? '' : ', $args'; - var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); - return '{\n ${lines.join(",\n ")}\n }'; - } - }); - - return '[${maps.join(",")}]'; - } } diff --git a/lib/tools/transformer/html_dart_references_generator.dart b/lib/tools/transformer/html_dart_references_generator.dart new file mode 100644 index 000000000..7af269616 --- /dev/null +++ b/lib/tools/transformer/html_dart_references_generator.dart @@ -0,0 +1,50 @@ +library angular.tools.transformer.html_dart_references_generator; + +import 'dart:async'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/assets.dart'; +import 'package:html5lib/parser.dart' as html5; + +/// Helper which allows subsequent transformers to know what HTML files are +/// referencing a Dart file. +/// +/// Currently this only allows a single HTML file to be referencing a Dart file. +class HtmlDartReferencesGenerator extends Transformer { + final TransformOptions options; + + HtmlDartReferencesGenerator(this.options); + + // Accept all HTML files. + String get allowedExtensions => '.html'; + + Future apply(Transform transform) { + var logger = transform.logger; + var asset = transform.primaryInput; + transform.addOutput(asset); + + return asset.readAsString().then((contents) { + var document = html5.parse(contents); + for (var tag in document.querySelectorAll('script')) { + if (tag.attributes['type'] != 'application/dart') continue; + + var src = tag.attributes['src']; + if (src == null) continue; + + var dartAssetId = uriToAssetId(asset.id, src, logger, tag.sourceSpan); + if (dartAssetId == null) continue; + + var htmlRefId = new AssetId(dartAssetId.package, + dartAssetId.path + '.html_reference'); + + return transform.readInputAsString(htmlRefId).then((contents) { + logger.error('Expected $dartAssetId to be referenced by a single ' + 'HTML file, was referenced by $contents and ${asset.id}.'); + }, onError: (e) { + transform.addOutput( + new Asset.fromString(htmlRefId, asset.id.path)); + }); + } + }); + } +} diff --git a/lib/tools/transformer/metadata_extractor.dart b/lib/tools/transformer/metadata_extractor.dart index 83cd56dd3..d8ef22f02 100644 --- a/lib/tools/transformer/metadata_extractor.dart +++ b/lib/tools/transformer/metadata_extractor.dart @@ -51,7 +51,7 @@ class AnnotatedType { /** * Helper which finds all libraries referenced within the provided AST. */ -class _LibraryCollector extends GeneralizingASTVisitor { +class _LibraryCollector extends GeneralizingAstVisitor { final Set libraries = new Set(); void visitSimpleIdentifier(SimpleIdentifier s) { var element = s.bestElement; @@ -132,10 +132,7 @@ class _AnnotationWriter { /** Writes an expression. */ bool _writeExpression(Expression expression) { if (expression is StringLiteral) { - var str = expression.stringValue - .replaceAll(r'\', r'\\') - .replaceAll('\'', '\\\''); - sink.write('\'$str\''); + sink.write(expression.toSource()); return true; } if (expression is ListLiteral) { @@ -179,27 +176,13 @@ class _AnnotationWriter { sink.write('${prefixes[variable.library]}${variable.name}'); return true; } - print('variable ${variable.runtimeType} $variable'); } - print('element ${element.runtimeType} $element'); } - if (expression is BooleanLiteral) { - sink.write(expression.value); + if (expression is BooleanLiteral || expression is DoubleLiteral || + expression is IntegerLiteral || expression is NullLiteral) { + sink.write(expression.toSource()); return true; } - if (expression is DoubleLiteral) { - sink.write(expression.value); - return true; - } - if (expression is IntegerLiteral) { - sink.write(expression.value); - return true; - } - if (expression is NullLiteral) { - sink.write('null'); - return true; - } - print('expression ${expression.runtimeType} $expression'); return false; } } @@ -246,6 +229,7 @@ class AnnotationExtractor { } } + /// Extracts all of the annotations for the specified class. AnnotatedType extractAnnotations(ClassElement cls) { if (resolver.getImportUri(cls.library, from: outputId) == null) { warn('Dropping annotations for ${cls.name} because the ' @@ -299,7 +283,7 @@ class AnnotationExtractor { /// Folds all AttrFieldAnnotations into the NgAnnotation annotation on the /// class. - _foldMemberAnnotations(Map memberAnnotations, + void _foldMemberAnnotations(Map memberAnnotations, AnnotatedType type) { // Filter down to NgAnnotation constructors. var ngAnnotations = type.annotations.where((a) { @@ -345,7 +329,8 @@ class AnnotationExtractor { // If we don't have a 'map' parameter yet, add one. if (mapArg == null) { var map = new MapLiteral(null, null, null, [], null); - var label = new Label(new SimpleIdentifier(stringToken('map')), + var label = new Label(new SimpleIdentifier( + new _GeneratedToken(TokenType.STRING, 'map')), new _GeneratedToken(TokenType.COLON, ':')); mapArg = new NamedExpression(label, map); annotation.arguments.arguments.add(mapArg); @@ -377,7 +362,8 @@ class AnnotationExtractor { }); } - Token stringToken(String str) => new _GeneratedToken(TokenType.STRING, str); + Token stringToken(String str) => + new _GeneratedToken(TokenType.STRING, '\'$str\''); void warn(String msg, Element element) { logger.warning(msg, asset: resolver.getSourceAssetId(element), @@ -396,7 +382,7 @@ class _GeneratedToken extends Token { * AST visitor which walks the current AST and finds all annotated * classes and members. */ -class _AnnotationVisitor extends GeneralizingASTVisitor { +class _AnnotationVisitor extends GeneralizingAstVisitor { final List allowedMemberAnnotations; final List classAnnotations = []; final Map> memberAnnotations = {}; diff --git a/lib/tools/transformer/metadata_generator.dart b/lib/tools/transformer/metadata_generator.dart index 0768cf662..74b3f97fc 100644 --- a/lib/tools/transformer/metadata_generator.dart +++ b/lib/tools/transformer/metadata_generator.dart @@ -17,8 +17,7 @@ class MetadataGenerator extends Transformer with ResolverTransformer { this.resolvers = resolvers; } - Future isPrimary(Asset input) => new Future.value( - options.isDartEntry(input.id)); + Future isPrimary(Asset input) => options.isDartEntry(input); void applyResolver(Transform transform, Resolver resolver) { var asset = transform.primaryInput; diff --git a/lib/tools/transformer/options.dart b/lib/tools/transformer/options.dart index ce681013c..f4de9794b 100644 --- a/lib/tools/transformer/options.dart +++ b/lib/tools/transformer/options.dart @@ -3,19 +3,15 @@ library angular.tools.transformer.options; import 'dart:async'; import 'package:barback/barback.dart'; +import 'package:analyzer/analyzer.dart' as analyzer; +import 'package:analyzer/src/generated/ast.dart'; +import 'package:code_transformers/resolver.dart'; import 'package:di/transformer/options.dart' as di; import 'package:path/path.dart' as path; /** Options used by Angular transformers */ class TransformOptions { - /** - * The file paths of the primary Dart entry point (main) for the application. - * This is used as the starting point to find all expressions used by the - * application. - */ - final Set dartEntries; - /** * List of html file paths which may contain Angular expressions. * The paths are relative to the package home and are represented using posix @@ -38,11 +34,9 @@ class TransformOptions { */ final di.TransformOptions diOptions; - TransformOptions({List dartEntries, - String sdkDirectory, List htmlFiles, + TransformOptions({String sdkDirectory, List htmlFiles, Map templateUriRewrites, - di.TransformOptions diOptions}) - : dartEntries = dartEntries.toSet(), + di.TransformOptions diOptions}) : sdkDirectory = sdkDirectory, htmlFiles = htmlFiles != null ? htmlFiles : [], templateUriRewrites = templateUriRewrites != null ? @@ -52,7 +46,5 @@ class TransformOptions { throw new ArgumentError('sdkDirectory must be provided.'); } - // Don't need to check package as transformers only run for primary package. - Future isDartEntry(AssetId id) => - new Future.value(dartEntries.contains(id.path)); + Future isDartEntry(Asset asset) => isPossibleDartEntry(asset); } diff --git a/lib/tools/transformer/referenced_uris.dart b/lib/tools/transformer/referenced_uris.dart new file mode 100644 index 000000000..505806631 --- /dev/null +++ b/lib/tools/transformer/referenced_uris.dart @@ -0,0 +1,259 @@ +library angular.tools.transformers.referenced_uris; + +import 'dart:async'; + +import 'package:analyzer/src/generated/ast.dart'; +import 'package:analyzer/src/generated/element.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; +import 'package:code_transformers/resolver.dart'; +import 'package:path/path.dart' as path; + +/// Gathers the contents of all URIs which are referenced by the contents of +/// the application. +/// Returns a map from URI to contents. +Future> gatherReferencedUris(Transform transform, + Resolver resolver, TransformOptions options, + {bool skipNonCached: false, bool templatesOnly: false}) { + return new _Processor(transform, resolver, options, skipNonCached, + templatesOnly).process(); +} + +class _Processor { + final Transform transform; + final Resolver resolver; + final TransformOptions options; + final Map templateUriRewrites = {}; + final bool skipNonCached; + final bool templatesOnly; + + ConstructorElement cacheAnnotation; + ConstructorElement componentAnnotation; + + static const String cacheAnnotationName = + 'angular.template_cache_annotation.NgTemplateCache'; + static const String componentAnnotationName = 'angular.core.NgComponent'; + + _Processor(this.transform, this.resolver, this.options, this.skipNonCached, + this.templatesOnly) { + for (var key in options.templateUriRewrites.keys) { + templateUriRewrites[new RegExp(key)] = options.templateUriRewrites[key]; + } + } + + /// Gathers the contents of all URIs which are to be cached. + /// Returns a map from URI to contents. + Future> process() { + var cacheAnnotationType = resolver.getType(cacheAnnotationName); + if (cacheAnnotationType != null && + cacheAnnotationType.unnamedConstructor != null) { + cacheAnnotation = cacheAnnotationType.unnamedConstructor; + } + + var componentAnnotationType = resolver.getType(componentAnnotationName); + if (componentAnnotationType != null && + componentAnnotationType.unnamedConstructor != null) { + componentAnnotation = componentAnnotationType.unnamedConstructor; + } else { + logger.warning('Unable to resolve $componentAnnotationName.'); + } + + var annotations = resolver.libraries + .expand((lib) => lib.units) + .expand((unit) => unit.types) + .where((type) => type.node != null) + .expand(_AnnotatedElement.fromElement) + .where((e) => + (e.annotation.element == cacheAnnotation || + e.annotation.element == componentAnnotation)) + .toList(); + + var uriToEntry = {}; + annotations.where((anno) => anno.annotation.element == componentAnnotation) + .expand(processComponentAnnotation) + .forEach((entry) { + uriToEntry[entry.uri] = entry; + }); + if (!templatesOnly) { + annotations.where((anno) => anno.annotation.element == cacheAnnotation) + .expand(processCacheAnnotation) + .forEach((entry) { + uriToEntry[entry.uri] = entry; + }); + } + + var futures = uriToEntry.values.map(cacheEntry); + + return Future.wait(futures).then((_) { + var uriToContents = {}; + for (var entry in uriToEntry.values) { + if (entry.contents == null) continue; + + uriToContents[entry.uri] = entry.contents; + } + return uriToContents; + }); + } + + /// Extracts the cacheable URIs from the NgComponent annotation. + List<_CacheEntry> processComponentAnnotation(_AnnotatedElement annotation) { + var entries = <_CacheEntry>[]; + if (skipNonCached && isCachingSuppressed(annotation.element)) { + return entries; + } + for (var arg in annotation.annotation.arguments.arguments) { + if (arg is NamedExpression) { + var paramName = arg.name.label.name; + if (paramName == 'templateUrl') { + var entry = extractString('templateUrl', arg.expression, + annotation.element); + if (entry != null) { + entries.add(entry); + } + } else if (paramName == 'cssUrl' && !templatesOnly) { + entries.addAll(extractListOrString(paramName, arg.expression, + annotation.element)); + } + } + } + + return entries; + } + + bool isCachingSuppressed(Element e) { + if (cacheAnnotation == null) return false; + AnnotatedNode node = e.node; + for (var annotation in node.metadata) { + if (annotation.element == cacheAnnotation) { + for (var arg in annotation.arguments.arguments) { + if (arg is NamedExpression && arg.name.label.name == 'cache') { + var value = arg.expression; + if (value is! BooleanLiteral) { + warn('Expected boolean literal for NgTemplateCache.cache', e); + return false; + } + return !value.value; + } + } + } + } + return false; + } + + List<_CacheEntry> processCacheAnnotation(_AnnotatedElement annotation) { + var entries = <_CacheEntry>[]; + for (var arg in annotation.annotation.arguments.arguments) { + if (arg is NamedExpression) { + var paramName = arg.name.label.name; + if (paramName == 'preCacheUrls') { + entries.addAll(extractListOrString(paramName, arg.expression, + annotation.element)); + } + } + } + return entries; + } + + List<_CacheEntry> extractListOrString(String paramName, + Expression expression, Element element) { + var entries = []; + if (expression is StringLiteral) { + var entry = uriToEntry(expression.stringValue, element); + if (entry != null) { + entries.add(entry); + } + } else if (expression is ListLiteral) { + for (var value in expression.elements) { + if (value is! StringLiteral) { + warn('Expected a string literal in $paramName', element); + continue; + } + var entry = uriToEntry(value.stringValue, element); + if (entry != null) { + entries.add(entry); + } + } + } else { + warn('$paramName must be a string or list literal.', element); + } + return entries; + } + + _CacheEntry extractString(String paramName, Expression expression, + Element element) { + if (expression is StringLiteral) { + return uriToEntry(expression.stringValue, element); + } + warn('$paramName must be a string literal.', element); + return null; + } + + Future<_CacheEntry> cacheEntry(_CacheEntry entry) { + return transform.readInputAsString(entry.assetId).then((contents) { + if (contents == null) { + warn('Unable to find ${entry.uri} at ${entry.assetId}', entry.element); + } + entry.contents = contents; + return entry; + }); + } + + _CacheEntry uriToEntry(String uri, Element reference) { + uri = rewriteUri(uri); + if (Uri.parse(uri).scheme != '') { + warn('Cannot cache non-local URIs. $uri', reference); + return null; + } + if (path.url.isAbsolute(uri)) { + var parts = path.posix.split(uri); + if (parts[1] == 'packages') { + var pkgPath = path.url.join('lib', path.url.joinAll(parts.skip(3))); + return new _CacheEntry(uri, reference, new AssetId(parts[2], pkgPath)); + } + warn('Cannot cache non-package absolute URIs. $uri', reference); + return null; + } + var assetId = new AssetId(transform.primaryInput.id.package, uri); + return new _CacheEntry(uri, reference, assetId); + } + + String rewriteUri(String uri) { + templateUriRewrites.forEach((regexp, replacement) { + uri = uri.replaceFirst(regexp, replacement); + }); + return uri; + } + + void warn(String msg, Element element) { + logger.warning(msg, asset: resolver.getSourceAssetId(element), + span: resolver.getSourceSpan(element)); + } + + TransformLogger get logger => transform.logger; +} + +/// Wrapper for data related to a single cache entry. +class _CacheEntry { + final String uri; + final Element element; + final AssetId assetId; + String contents; + + _CacheEntry(this.uri, this.element, this.assetId); +} + +/// Wrapper for annotation AST nodes to track the element they were declared on. +class _AnnotatedElement { + /// The annotation node. + final Annotation annotation; + /// The element which the annotation was declared on. + final Element element; + + _AnnotatedElement(this.annotation, this.element); + + static Iterable<_AnnotatedElement> fromElement(Element element) { + AnnotatedNode node = element.node; + return node.metadata.map( + (annotation) => new _AnnotatedElement(annotation, element)); + } +} diff --git a/lib/tools/transformer/static_angular_generator.dart b/lib/tools/transformer/static_angular_generator.dart index 5b0132571..8cd227a62 100644 --- a/lib/tools/transformer/static_angular_generator.dart +++ b/lib/tools/transformer/static_angular_generator.dart @@ -17,13 +17,13 @@ class StaticAngularGenerator extends Transformer with ResolverTransformer { this.resolvers = resolvers; } - Future isPrimary(Asset input) => new Future.value( - options.isDartEntry(input.id)); + Future isPrimary(Asset input) => options.isDartEntry(input); void applyResolver(Transform transform, Resolver resolver) { var asset = transform.primaryInput; - var dynamicApp = resolver.getType('angular.dynamic.NgDynamicApp'); + var dynamicApp = + resolver.getLibraryFunction('angular.dynamic.ngDynamicApp'); if (dynamicApp == null) { // No dynamic app imports, exit. transform.addOutput(transform.primaryInput); @@ -31,7 +31,7 @@ class StaticAngularGenerator extends Transformer with ResolverTransformer { } var id = asset.id; - var lib = resolver.entryLibrary; + var lib = resolver.getLibrary(id); var transaction = resolver.createTextEditTransaction(lib); var unit = lib.definingCompilationUnit.node; @@ -44,8 +44,8 @@ class StaticAngularGenerator extends Transformer with ResolverTransformer { } } - var dynamicToStatic = new _NgDynamicToStaticVisitor( - dynamicApp.unnamedConstructor, transaction); + var dynamicToStatic = + new _NgDynamicToStaticVisitor(dynamicApp, transaction); unit.accept(dynamicToStatic); var generatedFilePrefix = '${path.url.basenameWithoutExtension(id.path)}'; @@ -74,28 +74,23 @@ void _addImport(TextEditTransaction transaction, CompilationUnit unit, transaction.edit(last.end, last.end, '\nimport \'$uri\' as $prefix;'); } -class _NgDynamicToStaticVisitor extends GeneralizingASTVisitor { - final ConstructorElement constructor; +class _NgDynamicToStaticVisitor extends GeneralizingAstVisitor { + final Element ngDynamicFn; final TextEditTransaction transaction; - _NgDynamicToStaticVisitor(this.constructor, this.transaction); + _NgDynamicToStaticVisitor(this.ngDynamicFn, this.transaction); - visitInstanceCreationExpression(InstanceCreationExpression node) { - if (node.staticElement == constructor) { - var ctorName = node.constructorName; - var ctorStr = ctorName.toString(); - var prefix = ctorStr.indexOf('.') == -1 ? '' : - ctorStr.substring(0, ctorStr.indexOf('.') + 1); + visitMethodInvocation(MethodInvocation m) { + if (m.methodName.bestElement == ngDynamicFn) { + transaction.edit(m.methodName.beginToken.offset, + m.methodName.endToken.end, 'ngStaticApp'); - transaction.edit(ctorName.beginToken.offset, ctorName.end, - '${prefix}NgStaticApp'); - - var args = node.argumentList; + var args = m.argumentList; transaction.edit(args.beginToken.offset + 1, args.end - 1, 'generated_static_injector.factories, ' 'generated_static_metadata.typeAnnotations, ' 'generated_static_expressions.getters, ' - 'new generated_static_expressions.StaticClosureMap()'); + 'generated_static_expressions.setters'); } - return super.visitNode(node); + super.visitMethodInvocation(m); } } diff --git a/lib/transformer.dart b/lib/transformer.dart index 43a2648ed..9fb63ae38 100644 --- a/lib/transformer.dart +++ b/lib/transformer.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:angular/tools/transformer/expression_generator.dart'; import 'package:angular/tools/transformer/metadata_generator.dart'; import 'package:angular/tools/transformer/static_angular_generator.dart'; +import 'package:angular/tools/transformer/html_dart_references_generator.dart'; import 'package:angular/tools/transformer/options.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; @@ -39,11 +40,15 @@ TransformOptions _parseSettings(Map args) { 'angular.core.NgFilter']; annotations.addAll(_readStringListValue(args, 'injectable_annotations')); + // List of types which are otherwise not indicated as being injectable. + // Should all of these have @NgInjectableService? var injectedTypes = [ - 'perf_api.Profiler', - 'angular.core.RootScope', 'angular.core.AstParser', - 'angular.core.dom.NgAnimate']; + 'angular.core.RootScope', + 'angular.core.dom.NgAnimate', + 'angular.core.dom.NgElement', + 'perf_api.Profiler', + ]; injectedTypes.addAll(_readStringListValue(args, 'injected_types')); var sdkDir = _readStringValue(args, 'dart_sdk', required: false); @@ -52,16 +57,12 @@ TransformOptions _parseSettings(Map args) { sdkDir = path.dirname(path.dirname(Platform.executable)); } - var dartEntries = _readStringListValue(args, 'dart_entries'); - var diOptions = new di.TransformOptions( - dartEntries: dartEntries, injectableAnnotations: annotations, injectedTypes: injectedTypes, sdkDirectory: sdkDir); return new TransformOptions( - dartEntries: dartEntries, htmlFiles: _readStringListValue(args, 'html_files'), sdkDirectory: sdkDir, templateUriRewrites: _readStringMapValue(args, 'template_uri_rewrites'), @@ -126,6 +127,7 @@ Map _readStringMapValue(Map args, String name) { List> _createPhases(TransformOptions options) { var resolvers = new Resolvers(options.sdkDirectory); return [ + [new HtmlDartReferencesGenerator(options)], [new ExpressionGenerator(options, resolvers)], [new di.InjectorGenerator(options.diOptions, resolvers)], [new MetadataGenerator(options, resolvers)], diff --git a/pubspec.lock b/pubspec.lock index 0a9745430..1ade64f67 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.12.2" + version: "0.13.0-dev.9" args: description: args source: hosted @@ -24,18 +24,18 @@ packages: code_transformers: description: code_transformers source: hosted - version: "0.0.1-dev.2" + version: "0.0.1-dev.3" collection: description: collection source: hosted version: "0.9.1" di: description: - ref: null - resolved-ref: "88e0d48101517e1d3bc84f9d38d4a1b619db65aa" - url: "https://github.com/angular/di.dart.git" + ref: angular_tweaks + resolved-ref: "8e2f7cb482d120b51fa6b8704c3bfe02df526b95" + url: "git://github.com/blois/di.dart.git" source: git - version: "0.0.33" + version: "0.0.34" html5lib: description: html5lib source: hosted diff --git a/pubspec.yaml b/pubspec.yaml index c8a4a4e22..81556fa2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,9 +12,9 @@ homepage: https://angulardart.org environment: sdk: '>=1.2.0' dependencies: - analyzer: '>=0.12.0 <0.13.0' + analyzer: '>=0.13.0 <0.14.0' browser: '>=0.8.7 <0.10.0' - code_transformers: '>=0.0.1-dev.2 <0.1.0' + code_transformers: '>=0.0.1 <0.1.0' collection: '>=0.9.1 <1.0.0' di: '>=0.0.32 <0.1.0' html5lib: '>=0.8.7 <0.10.0' @@ -28,4 +28,10 @@ dev_dependencies: dependency_overrides: di: - git: https://github.com/angular/di.dart.git + git: + url: git://github.com/blois/di.dart.git + ref: angular_tweaks + code_transformers: 0.0.1-dev.3 + analyzer: '>=0.13.0-dev <0.14.0' + + diff --git a/test/tools/transformer/expression_generator_spec.dart b/test/tools/transformer/expression_generator_spec.dart index d36a15426..7a6737ed3 100644 --- a/test/tools/transformer/expression_generator_spec.dart +++ b/test/tools/transformer/expression_generator_spec.dart @@ -1,68 +1,141 @@ library angular.test.tools.transformer.expression_extractor_spec; -import 'package:angular/tools/transformer/options.dart'; +import 'dart:async'; + import 'package:angular/tools/transformer/expression_generator.dart'; +import 'package:angular/tools/transformer/html_dart_references_generator.dart'; +import 'package:angular/tools/transformer/options.dart'; +import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:code_transformers/tests.dart' as tests; + import '../../jasmine_syntax.dart'; main() { describe('ExpressionGenerator', () { var htmlFiles = []; + var templateUriRewrites = {}; var options = new TransformOptions( - dartEntries: ['web/main.dart'], htmlFiles: htmlFiles, + templateUriRewrites: templateUriRewrites, sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); var phases = [ + [new HtmlDartReferencesGenerator(options)], [new ExpressionGenerator(options, resolvers)] ]; it('should extract expressions', () { - htmlFiles.add('web/index.html'); - return tests.applyTransformers(phases, + return generates(phases, inputs: { 'a|web/main.dart': ''' -library foo; -''', + import 'package:angular/angular.dart'; + + main() {} ''', 'a|web/index.html': ''' -
{{some.getter}}
-''' +
{{some.getter}}
+ ''', + 'angular|lib/angular.dart': libAngular, }, - results: { - 'a|web/main_static_expressions.dart': ''' -$header -final Map getters = { - r"some": (o) => o.some, - r"getter": (o) => o.getter -}; -final Map setters = { - r"some": (o, v) => o.some = v, - r"getter": (o, v) => o.getter = v -}; -final List> functions = []; -''' - }).whenComplete(() { - htmlFiles.clear(); - }); + getters: ['some', 'getter'], + setters: ['some', 'getter']); + }); + + it('should extract functions as getters', () { + return generates(phases, + inputs: { + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + main() {} ''', + 'a|web/index.html': ''' +
{{some.method()}}
+ ''', + 'angular|lib/angular.dart': libAngular, + }, + getters: ['some', 'method'], + setters: ['some']); + }); + + it('should follow templateUris', () { + return generates(phases, + inputs: { + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgComponent( + templateUrl: 'lib/foo.html', + selector: 'my-component') + class FooComponent {} + + main() {} + ''', + 'a|lib/foo.html': ''' +
{{template.contents}}
''', + 'a|web/index.html': ''' + ''', + 'angular|lib/angular.dart': libAngular, + }, + getters: ['template', 'contents'], + setters: ['template', 'contents']); + }); + + it('should apply additional HTML files', () { + htmlFiles.add('web/dummy.html'); + return generates(phases, + inputs: { + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + main() {} + ''', + 'a|web/dummy.html': ''' +
{{contents}}
''', + 'a|web/index.html': ''' + ''', + 'angular|lib/angular.dart': libAngular, + }, + getters: ['contents'], + setters: ['contents']).whenComplete(() { + htmlFiles.clear(); + }); }); }); } +Future generates(List> phases, + {Map inputs, List getters, List setters, + Iterable messages: const []}) { + + var buffer = new StringBuffer(); + buffer.write(header); + buffer.write('final Map getters = {\n'); + buffer.write(getters.map((g) => ' r"$g": (o) => o.$g').join(',\n')); + buffer.write('\n};\n'); + buffer.write('final Map setters = {\n'); + buffer.write(setters.map((s) => ' r"$s": (o, v) => o.$s = v').join(',\n')); + buffer.write('\n};\n'); + + return tests.applyTransformers(phases, + inputs: inputs, + results: { + 'a|web/main_static_expressions.dart': buffer.toString() + }, + messages: messages); +} + const String header = ''' library a.web.main.generated_expressions; -import 'package:angular/angular.dart'; -import 'package:angular/core/parser/dynamic_parser.dart' show ClosureMap; +import 'package:angular/change_detection/change_detection.dart'; + +'''; -Module get expressionModule => new Module() - ..value(ClosureMap, new StaticClosureMap()); +const String libAngular = ''' +library angular.core; -class StaticClosureMap extends ClosureMap { - Getter lookupGetter(String name) => getters[name]; - Setter lookupSetter(String name) => setters[name]; - lookupFunction(String name, int arity) - => (arity < functions.length) ? functions[arity][name] : null; +class NgComponent { + const NgComponent({String templateUrl, String selector}); } '''; diff --git a/test/tools/transformer/metadata_generator_spec.dart b/test/tools/transformer/metadata_generator_spec.dart index 61e9ebe07..290168d4f 100644 --- a/test/tools/transformer/metadata_generator_spec.dart +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -12,9 +12,7 @@ import '../../jasmine_syntax.dart'; main() { describe('MetadataGenerator', () { - var options = new TransformOptions( - dartEntries: ['web/main.dart'], - sdkDirectory: dartSdkDirectory); + var options = new TransformOptions(sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -42,6 +40,7 @@ main() { @NgTwoWay('two-way-stuff') String get twoWayStuff => null; } + main() {} ''' }, imports: [ @@ -50,7 +49,8 @@ main() { ], classes: { 'import_0.Engine': [ - 'const import_1.NgDirective(selector: \'[*=/{{.*}}/]\', map: const {' + 'const import_1.NgDirective(selector: r\'[*=/{{.*}}/]\', ' + 'map: const {' '\'another-expression\': \'=>anotherExpression\', ' '\'callback\': \'&callback\', ' '\'two-way-stuff\': \'<=>twoWayStuff\'' @@ -72,10 +72,11 @@ main() { @NgOneWay('another-expression') set callback(Function) {} } + main() {} ''' }, messages: ['warning: callback can only have one annotation. ' - '(main.dart 3 18)']); + '(web/main.dart 3 18)']); }); it('should warn on duplicated annotations', () { @@ -90,6 +91,7 @@ main() { @NgOneWay('another-expression') set anotherExpression(Function) {} } + main() {} ''' }, imports: [ @@ -104,7 +106,7 @@ main() { }, messages: ['warning: Directive @NgOneWay(\'another-expression\') ' 'already contains an entry for \'another-expression\' ' - '(main.dart 2 16)' + '(web/main.dart 2 16)' ]); }); @@ -123,6 +125,7 @@ main() { @NgTwoWay('two-way-stuff') String get twoWayStuff => null; } + main() {} ''' }, imports: [ @@ -150,12 +153,13 @@ main() { set callback(Function) {} @NgOneWay('another-expression') - get callback() {} + get callback => null; } + main() {} ''' }, messages: ['warning: callback can only have one annotation. ' - '(main.dart 3 18)']); + '(web/main.dart 3 18)']); }); it('should extract map arguments', () { @@ -167,6 +171,8 @@ main() { @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) class Engine {} + + main() {} ''' }, imports: [ @@ -175,7 +181,8 @@ main() { ], classes: { 'import_0.Engine': [ - 'const import_1.NgDirective(map: const {\'ng-value\': \'&ngValue\', \'key\': \'value\'})', + 'const import_1.NgDirective(map: const {\'ng-value\': ' + '\'&ngValue\', \'key\': \'value\'})', ] }); }); @@ -189,6 +196,8 @@ main() { @NgDirective(publishTypes: const [TextChangeListener]) class Engine {} + + main() {} ''' }, imports: [ @@ -197,7 +206,8 @@ main() { ], classes: { 'import_0.Engine': [ - 'const import_1.NgDirective(publishTypes: const [import_1.TextChangeListener,])', + 'const import_1.NgDirective(publishTypes: const ' + '[import_1.TextChangeListener,])', ] }); }); @@ -214,6 +224,8 @@ main() { @NgOneWay(1) @NgOneWay(null) class Engine {} + + main() {} ''' }, imports: [ @@ -242,6 +254,8 @@ main() { @NgDirective(publishTypes: const [Foo]) class Car {} + + main() {} ''' }, imports: [ @@ -257,9 +271,11 @@ main() { ] }, messages: [ - 'warning: Unable to serialize annotation @Foo. (main.dart 2 16)', + 'warning: Unable to serialize annotation @Foo. ' + '(web/main.dart 2 16)', 'warning: Unable to serialize annotation ' - '@NgDirective(publishTypes: const [Foo]). (main.dart 5 16)', + '@NgDirective(publishTypes: const [Foo]). ' + '(web/main.dart 5 16)', ]); }); @@ -273,6 +289,8 @@ main() { @NgDirective(publishTypes: const [Car]) class Engine {} + + main() {} ''', 'a|lib/b.dart': ''' class Car {} @@ -303,6 +321,7 @@ main() { print('something'); } } + main() {} ''', }); }); @@ -317,6 +336,8 @@ main() { @NgOneWay('foo\' \\') class Engine { } + + main() {} ''', }, imports: [ @@ -330,6 +351,33 @@ main() { }); }); + it('maintains string formatting', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': r''' + import 'package:angular/angular.dart'; + + @NgOneWay(r"""multiline + string""") + class Engine { + } + + main() {} + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + r'''const import_1.NgOneWay(r"""multiline + string""")''', + ] + }); + }); + it('should reference static and global properties', () { return generates(phases, inputs: { @@ -342,6 +390,8 @@ main() { class Engine {} const int CONST_VALUE = 2; + + main() {} ''', }, imports: [ @@ -350,8 +400,9 @@ main() { ], classes: { 'import_0.Engine': [ - '''const import_1.NgDirective(visibility: import_1.NgDirective.CHILDREN_VISIBILITY)''', - '''const import_1.NgDirective(visibility: import_0.CONST_VALUE)''', + 'const import_1.NgDirective(visibility: ' + 'import_1.NgDirective.CHILDREN_VISIBILITY)', + 'const import_1.NgDirective(visibility: import_0.CONST_VALUE)', ] }); }); @@ -372,11 +423,13 @@ main() { const _Foo(); } const _Foo _foo = const _Foo(); + + main() {} ''', }, messages: [ - 'warning: Annotation @_Foo() is not public. (main.dart 2 16)', - 'warning: Annotation @_foo is not public. (main.dart 2 16)', + 'warning: Annotation @_Foo() is not public. (web/main.dart 2 16)', + 'warning: Annotation @_foo is not public. (web/main.dart 2 16)', ]); }); @@ -396,6 +449,8 @@ main() { const Foo.bar(); const Foo._private(); } + + main() {} ''', }, imports: [ @@ -408,7 +463,7 @@ main() { }, messages: [ 'warning: Annotation @Foo._private() is not public. ' - '(main.dart 2 16)', + '(web/main.dart 2 16)', ]); }); }); diff --git a/test/tools/transformer/static_angular_generator_spec.dart b/test/tools/transformer/static_angular_generator_spec.dart index c9ece2c3a..ad4bf34a6 100644 --- a/test/tools/transformer/static_angular_generator_spec.dart +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -1,10 +1,7 @@ -library angular.test.tools.transformer.metadata_generator_spec; - -import 'dart:async'; +library angular.test.tools.transformer.static_angular_generator_spec; import 'package:angular/tools/transformer/options.dart'; import 'package:angular/tools/transformer/static_angular_generator.dart'; -import 'package:barback/barback.dart'; import 'package:code_transformers/resolver.dart'; import 'package:code_transformers/tests.dart' as tests; @@ -13,7 +10,6 @@ import '../../jasmine_syntax.dart'; main() { describe('StaticAngularGenerator', () { var options = new TransformOptions( - dartEntries: ['web/main.dart'], sdkDirectory: dartSdkDirectory); var resolvers = new Resolvers(dartSdkDirectory); @@ -22,7 +18,7 @@ main() { [new StaticAngularGenerator(options, resolvers)] ]; - it('should modify NgDynamicApp', () { + it('should modify ngDynamicApp', () { return tests.applyTransformers(phases, inputs: { 'angular|lib/angular_dynamic.dart': libAngularDynamic, @@ -34,7 +30,7 @@ import 'package:di/di.dart' show Module; class MyModule extends Module {} main() { - var app = new NgDynamicApp() + var app = ngDynamicApp() .addModule(new MyModule()) .run(); } @@ -51,7 +47,7 @@ import 'main_static_injector.dart' as generated_static_injector; class MyModule extends Module {} main() { - var app = new NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + var app = ngStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters) .addModule(new MyModule()) .run(); } @@ -71,7 +67,7 @@ import 'package:di/di.dart' show Module; class MyModule extends Module {} main() { - var app = new ng.NgDynamicApp() + var app = ng.ngDynamicApp() .addModule(new MyModule()) .run(); } @@ -88,7 +84,7 @@ import 'main_static_injector.dart' as generated_static_injector; class MyModule extends Module {} main() { - var app = new ng.NgStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, new generated_static_expressions.StaticClosureMap()) + var app = ng.ngStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters) .addModule(new MyModule()) .run(); } @@ -101,9 +97,10 @@ main() { const String libAngularDynamic = ''' -library angular.dynamic +library angular.dynamic; +class _NgDynamicApp {} -class NgDynamicApp {}; +ngDynamicApp() => new _NgDynamicApp(); '''; const String libDI = '''