diff --git a/example/pubspec.lock b/example/pubspec.lock index 08762dadd..cd9f21030 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.13.0-dev.9" angular: description: path: ".." @@ -14,18 +14,28 @@ packages: args: description: args source: hosted - version: "0.9.0" + version: "0.10.0+1" + 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.3" collection: description: collection source: hosted version: "0.9.1" di: - description: di - source: hosted + description: + path: "../../di.dart" + relative: true + source: path version: "0.0.33" html5lib: description: html5lib @@ -34,11 +44,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 @@ -67,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 0aff3ae1d..ca29799df 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,3 +5,6 @@ dependencies: path: ../ browser: any unittest: any + +transformers: +- 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 d2ea4024f..b8d64f30b 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'; @@ -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,79 +126,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) + 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/example/web/todo.dart b/example/web/todo.dart index d3a5c674f..01b6fa2eb 100644 --- a/example/web/todo.dart +++ b/example/web/todo.dart @@ -3,18 +3,9 @@ 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'; -// 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; @@ -40,6 +31,7 @@ abstract class ServerController { // An implementation of ServerController that does nothing. +@NgInjectableService() class NoServerController implements ServerController { init(TodoController todo) { } } @@ -47,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/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/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/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 new file mode 100644 index 000000000..0f2961209 --- /dev/null +++ b/lib/tools/transformer/expression_generator.dart @@ -0,0 +1,178 @@ +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/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:angular/tools/transformer/referenced_uris.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:path/path.dart' as path; + +/** + * 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 Transformer with ResolverTransformer { + final TransformOptions options; + + ExpressionGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => options.isDartEntry(input); + + 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, resolver) + .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 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())); + + transform.addOutput(asset); + }); + } + + /** + * Gets a stream consisting of the contents of all HTML source files to be + * scoured for expressions. + */ + Stream _getHtmlSources(Transform transform, Resolver resolver) { + var id = transform.primaryInput.id; + + var controller = new StreamController(); + 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) { + var libPath = path.withoutExtension(id.path).replaceAll('/', '.'); + sink.write(''' +library ${id.package}.$libPath.generated_expressions; + +import 'package:angular/change_detection/change_detection.dart'; + +'''); +} + +class _LibrarySourceCrawler implements SourceCrawler { + final Iterable 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(generateClosures(backend.properties, backend.calls)); + } + + String generateClosures(Set properties, + Map> calls) { + var getters = new Set.from(properties.toList())..addAll(calls.keys); + return ''' +final Map getters = ${generateGetterMap(getters)}; +final Map setters = ${generateSetterMap(properties)}; +'''; + } + + 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}'; + } +} + 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 new file mode 100644 index 000000000..d8ef22f02 --- /dev/null +++ b/lib/tools/transformer/metadata_extractor.dart @@ -0,0 +1,411 @@ +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'; + +class AnnotatedType { + final ClassElement type; + Iterable annotations; + + 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); + } + libs.addAll(libCollector.libraries); + + return libs; + } + + void writeClassAnnotations(StringBuffer sink, TransformLogger logger, + Resolver resolver, Map prefixes) { + sink.write(' ${prefixes[type.library]}${type.name}: const [\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'); + } +} + +/** + * 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) { + sink.write(expression.toSource()); + 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; + } + } + } + if (expression is BooleanLiteral || expression is DoubleLiteral || + expression is IntegerLiteral || expression is NullLiteral) { + sink.write(expression.toSource()); + return true; + } + return false; + } +} + +class AnnotationExtractor { + final TransformLogger logger; + final Resolver resolver; + final AssetId outputId; + + static const List _angularAnnotationNames = const [ + 'angular.core.NgAttr', + 'angular.core.NgOneWay', + 'angular.core.NgOneWayOneTime', + 'angular.core.NgTwoWay', + '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, this.outputId) { + 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); + } + ngAnnotationType = resolver.getType('angular.core.NgAnnotation'); + if (ngAnnotationType == null) { + logger.warning('Unable to resolve NgAnnotation, ' + 'skipping member annotations.'); + } + } + + /// 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 ' + 'containing file cannot be imported (must be in a lib folder).', 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) { + warn('Annotation $annotation is not public.', + annotation.parent.element); + return false; + } + if (element is ConstructorElement && + !element.enclosingElement.isPublic) { + 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) { + warn('$memberName can only have one annotation.', + annotations[0].parent.element); + return; + } + + memberAnnotations[memberName] = annotations[0]; + }); + + if (memberAnnotations.isNotEmpty) { + _foldMemberAnnotations(memberAnnotations, type); + } + + if (type.annotations.isEmpty) return null; + + return type; + } + + /// Folds all AttrFieldAnnotations into the NgAnnotation annotation on the + /// class. + void _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( + new _GeneratedToken(TokenType.STRING, '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); +} + + +/** + * 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..74b3f97fc --- /dev/null +++ b/lib/tools/transformer/metadata_generator.dart @@ -0,0 +1,120 @@ +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'; + +class MetadataGenerator extends Transformer with ResolverTransformer { + final TransformOptions options; + + MetadataGenerator(this.options, Resolvers resolvers) { + this.resolvers = resolvers; + } + + Future isPrimary(Asset input) => options.isDartEntry(input); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + 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); + + 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, from: outputId); + 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); + + transform.addOutput( + new Asset.fromString(outputId, outputBuffer.toString())); + transform.addOutput(asset); + } +} + +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 MetadataExtractor; +import 'package:di/di.dart' show Module; + +'''); +} + +void _writePreamble(StringSink sink) { + sink.write(''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = typeAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +'''); +} + +void _writeClassPreamble(StringSink sink) { + sink.write(''' +final Map typeAnnotations = { +'''); +} + +void _writeClassEpilogue(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..f4de9794b --- /dev/null +++ b/lib/tools/transformer/options.dart @@ -0,0 +1,50 @@ +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 { + + /** + * 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 sdkDirectory, List htmlFiles, + Map templateUriRewrites, + di.TransformOptions diOptions}) : + sdkDirectory = sdkDirectory, + htmlFiles = htmlFiles != null ? htmlFiles : [], + templateUriRewrites = templateUriRewrites != null ? + templateUriRewrites : {}, + diOptions = diOptions { + if (sdkDirectory == null) + throw new ArgumentError('sdkDirectory must be provided.'); + } + + 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 new file mode 100644 index 000000000..8cd227a62 --- /dev/null +++ b/lib/tools/transformer/static_angular_generator.dart @@ -0,0 +1,96 @@ +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) => options.isDartEntry(input); + + void applyResolver(Transform transform, Resolver resolver) { + var asset = transform.primaryInput; + + var dynamicApp = + resolver.getLibraryFunction('angular.dynamic.ngDynamicApp'); + if (dynamicApp == null) { + // No dynamic app imports, exit. + transform.addOutput(transform.primaryInput); + return; + } + + var id = asset.id; + var lib = resolver.getLibrary(id); + 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, 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 Element ngDynamicFn; + final TextEditTransaction transaction; + _NgDynamicToStaticVisitor(this.ngDynamicFn, this.transaction); + + visitMethodInvocation(MethodInvocation m) { + if (m.methodName.bestElement == ngDynamicFn) { + transaction.edit(m.methodName.beginToken.offset, + m.methodName.endToken.end, 'ngStaticApp'); + + 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, ' + 'generated_static_expressions.setters'); + } + super.visitMethodInvocation(m); + } +} diff --git a/lib/transformer.dart b/lib/transformer.dart new file mode 100644 index 000000000..9fb63ae38 --- /dev/null +++ b/lib/transformer.dart @@ -0,0 +1,136 @@ +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/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: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')); + + // List of types which are otherwise not indicated as being injectable. + // Should all of these have @NgInjectableService? + var injectedTypes = [ + 'angular.core.AstParser', + '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); + if (sdkDir == null) { + // Assume the Pub executable is always coming from the SDK. + sdkDir = path.dirname(path.dirname(Platform.executable)); + } + + var diOptions = new di.TransformOptions( + injectableAnnotations: annotations, + injectedTypes: injectedTypes, + sdkDirectory: sdkDir); + + return new TransformOptions( + 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 HtmlDartReferencesGenerator(options)], + [new ExpressionGenerator(options, resolvers)], + [new di.InjectorGenerator(options.diOptions, resolvers)], + [new MetadataGenerator(options, resolvers)], + [new StaticAngularGenerator(options, resolvers)], + ]; +} diff --git a/pubspec.lock b/pubspec.lock index 07b511f97..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,14 +24,17 @@ 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: di - source: hosted + description: + ref: angular_tweaks + resolved-ref: "8e2f7cb482d120b51fa6b8704c3bfe02df526b95" + url: "git://github.com/blois/di.dart.git" + source: git version: "0.0.34" html5lib: description: html5lib diff --git a/pubspec.yaml b/pubspec.yaml index 63bbe5173..81556fa2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,8 +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 <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,13 @@ dependencies: dev_dependencies: benchmark_harness: '>=1.0.0' unittest: '>=0.8.7 <0.10.0' + +dependency_overrides: + di: + 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/_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_generator_spec.dart b/test/tools/transformer/expression_generator_spec.dart new file mode 100644 index 000000000..7a6737ed3 --- /dev/null +++ b/test/tools/transformer/expression_generator_spec.dart @@ -0,0 +1,141 @@ +library angular.test.tools.transformer.expression_extractor_spec; + +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( + htmlFiles: htmlFiles, + templateUriRewrites: templateUriRewrites, + sdkDirectory: dartSdkDirectory); + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new HtmlDartReferencesGenerator(options)], + [new ExpressionGenerator(options, resolvers)] + ]; + + it('should extract expressions', () { + return generates(phases, + inputs: { + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + main() {} ''', + 'a|web/index.html': ''' +
{{some.getter}}
+ ''', + 'angular|lib/angular.dart': libAngular, + }, + 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/change_detection/change_detection.dart'; + +'''; + +const String libAngular = ''' +library angular.core; + +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 new file mode 100644 index 000000000..290168d4f --- /dev/null +++ b/test/tools/transformer/metadata_generator_spec.dart @@ -0,0 +1,561 @@ +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('MetadataGenerator', () { + var options = new TransformOptions(sdkDirectory: dartSdkDirectory); + + var resolvers = new Resolvers(dartSdkDirectory); + + var phases = [ + [new MetadataGenerator(options, resolvers)] + ]; + + it('should extract member metadata', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.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; + } + main() {} + ''' + }, + imports: [ + 'import \'main.dart\' as import_0;', + 'import \'package:angular/angular.dart\' as import_1;', + ], + classes: { + 'import_0.Engine': [ + 'const import_1.NgDirective(selector: r\'[*=/{{.*}}/]\', ' + 'map: const {' + '\'another-expression\': \'=>anotherExpression\', ' + '\'callback\': \'&callback\', ' + '\'two-way-stuff\': \'<=>twoWayStuff\'' + '})', + 'proxy', + ] + }); + }); + + it('should warn on multiple annotations', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + @NgOneWay('another-expression') + set callback(Function) {} + } + main() {} + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(web/main.dart 3 18)']); + }); + + it('should warn on duplicated 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 { + @NgOneWay('another-expression') + set anotherExpression(Function) {} + } + main() {} + ''' + }, + 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\' ' + '(web/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; + } + main() {} + ''' + }, + 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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + @NgCallback('callback') + set callback(Function) {} + + @NgOneWay('another-expression') + get callback => null; + } + main() {} + ''' + }, + messages: ['warning: callback can only have one annotation. ' + '(web/main.dart 3 18)']); + }); + + it('should extract map arguments', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(map: const {'ng-value': '&ngValue', 'key': 'value'}) + class Engine {} + + main() {} + ''' + }, + 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 {\'ng-value\': ' + '\'&ngValue\', \'key\': \'value\'})', + ] + }); + }); + + it('should extract list arguments', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(publishTypes: const [TextChangeListener]) + class Engine {} + + main() {} + ''' + }, + imports: [ + 'import \'main.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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgOneWay(true) + @NgOneWay(1.0) + @NgOneWay(1) + @NgOneWay(null) + class Engine {} + + main() {} + ''' + }, + imports: [ + 'import \'main.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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @Foo + class Engine {} + + @NgDirective(publishTypes: const [Foo]) + class Car {} + + main() {} + ''' + }, + imports: [ + 'import \'main.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. ' + '(web/main.dart 2 16)', + 'warning: Unable to serialize annotation ' + '@NgDirective(publishTypes: const [Foo]). ' + '(web/main.dart 5 16)', + ]); + }); + + it('should extract types across libs', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + import 'package:a/b.dart'; + + @NgDirective(publishTypes: const [Car]) + class Engine {} + + main() {} + ''', + 'a|lib/b.dart': ''' + class Car {} + ''', + }, + imports: [ + 'import \'main.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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + class Engine { + Engine() { + @NgDirective() + print('something'); + } + } + main() {} + ''', + }); + }); + + it('properly escapes strings', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': r''' + import 'package:angular/angular.dart'; + + @NgOneWay('foo\' \\') + 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('foo\' \\')''', + ] + }); + }); + + 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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @NgDirective(visibility: NgDirective.CHILDREN_VISIBILITY) + @NgDirective(visibility: CONST_VALUE) + class Engine {} + + const int CONST_VALUE = 2; + + main() {} + ''', + }, + imports: [ + 'import \'main.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: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @_Foo() + @_foo + class Engine { + } + + class _Foo { + const _Foo(); + } + const _Foo _foo = const _Foo(); + + main() {} + ''', + }, + messages: [ + 'warning: Annotation @_Foo() is not public. (web/main.dart 2 16)', + 'warning: Annotation @_foo is not public. (web/main.dart 2 16)', + ]); + }); + + it('supports named constructors', () { + return generates(phases, + inputs: { + 'angular|lib/angular.dart': libAngular, + 'a|web/main.dart': ''' + import 'package:angular/angular.dart'; + + @Foo.bar() + @Foo._private() + class Engine { + } + + class Foo { + const Foo.bar(); + const Foo._private(); + } + + main() {} + ''', + }, + imports: [ + 'import \'main.dart\' as import_0;', + ], + classes: { + 'import_0.Engine': [ + '''const import_0.Foo.bar()''', + ] + }, + messages: [ + 'warning: Annotation @Foo._private() is not public. ' + '(web/main.dart 2 16)', + ]); + }); + }); +} + +Future generates(List> phases, + {Map inputs, Iterable imports: const [], + Map classes: const {}, + Iterable messages: const []}) { + + var buffer = new StringBuffer(); + buffer.write('$header\n'); + for (var i in imports) { + buffer.write('$i\n'); + } + buffer.write('$boilerPlate\n'); + for (var className in classes.keys) { + buffer.write(' $className: const [\n'); + for (var annotation in classes[className]) { + buffer.write(' $annotation,\n'); + } + buffer.write(' ],\n'); + } + + buffer.write('$footer\n'); + + return tests.applyTransformers(phases, + inputs: inputs, + results: { + 'a|web/main_static_metadata.dart': buffer.toString() + }, + messages: messages); +} + +const String header = ''' +library a.web.main.generated_metadata; + +import 'package:angular/angular.dart' show MetadataExtractor; +import 'package:di/di.dart' show Module; +'''; + +const String boilerPlate = ''' +Module get metadataModule => new Module() + ..value(MetadataExtractor, new _StaticMetadataExtractor()); + +class _StaticMetadataExtractor implements MetadataExtractor { + Iterable call(Type type) { + var annotations = typeAnnotations[type]; + if (annotations != null) { + return annotations; + } + return []; + } +} + +final Map typeAnnotations = {'''; + +const String footer = ''' +};'''; + + +const String libAngular = ''' +library angular.core; + +class NgAnnotation { + NgAnnotation({map: const {}}); +} + +class NgDirective extends NgAnnotation { + const NgDirective({selector, publishTypes, map, visibility}) : super(map: map); + + 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 {} +'''; 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..ad4bf34a6 --- /dev/null +++ b/test/tools/transformer/static_angular_generator_spec.dart @@ -0,0 +1,108 @@ +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:code_transformers/resolver.dart'; +import 'package:code_transformers/tests.dart' as tests; + +import '../../jasmine_syntax.dart'; + +main() { + describe('StaticAngularGenerator', () { + var options = new TransformOptions( + 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 = 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 = ngStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters) + .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 = 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 = ng.ngStaticApp(generated_static_injector.factories, generated_static_metadata.typeAnnotations, generated_static_expressions.getters, generated_static_expressions.setters) + .addModule(new MyModule()) + .run(); +} +''' + }); + }); + }); +} + + + +const String libAngularDynamic = ''' +library angular.dynamic; +class _NgDynamicApp {} + +ngDynamicApp() => new _NgDynamicApp(); +'''; + +const String libDI = ''' +class Module {} +''';