From 32e073b74fe62a118d017d62ee378f7f114798a2 Mon Sep 17 00:00:00 2001 From: Pavel Jbanov Date: Wed, 18 Dec 2013 16:54:40 -0800 Subject: [PATCH] feat(template_cache_generator): simple template cache generator --- bin/template_cache_generator.dart | 5 + lib/tools/template_cache_annotation.dart | 21 +++ lib/tools/template_cache_generator.dart | 201 +++++++++++++++++++++ test/io/all.dart | 2 + test/io/template_cache_generator_spec.dart | 36 ++++ test/io/test_files/templates/dont.html | 1 + test/io/test_files/templates/main.dart | 29 +++ test/io/test_files/templates/main.html | 1 + 8 files changed, 296 insertions(+) create mode 100644 bin/template_cache_generator.dart create mode 100644 lib/tools/template_cache_annotation.dart create mode 100644 lib/tools/template_cache_generator.dart create mode 100644 test/io/template_cache_generator_spec.dart create mode 100644 test/io/test_files/templates/dont.html create mode 100644 test/io/test_files/templates/main.dart create mode 100644 test/io/test_files/templates/main.html diff --git a/bin/template_cache_generator.dart b/bin/template_cache_generator.dart new file mode 100644 index 000000000..9ce9e3128 --- /dev/null +++ b/bin/template_cache_generator.dart @@ -0,0 +1,5 @@ +import 'package:angular/tools/template_cache_generator.dart' as generator; + +main(args) { + generator.main(args); +} diff --git a/lib/tools/template_cache_annotation.dart b/lib/tools/template_cache_annotation.dart new file mode 100644 index 000000000..111ed1a51 --- /dev/null +++ b/lib/tools/template_cache_annotation.dart @@ -0,0 +1,21 @@ +library angular.template_cache_annotation; + +/** + * Annotation which will control the caching behavior of objects in the + * template_cache. + * + * Primary use cases are: + * - Adding URLs to the cache which cannot be automatically gathered from + * NgComponent annotations. + * - Adding annotation to an NgComponent to remove it from the template cache. + */ +class NgTemplateCache { + // List of strings to add to the template cache. + final List preCacheUrls; + // Whether to cache these resources in the template cache. Primary use is to + // override the default caching behavior for NgComponent annotation. + final bool cache; + + const NgTemplateCache( + {this.preCacheUrls : const [], this.cache : true}); +} diff --git a/lib/tools/template_cache_generator.dart b/lib/tools/template_cache_generator.dart new file mode 100644 index 000000000..9987d28e9 --- /dev/null +++ b/lib/tools/template_cache_generator.dart @@ -0,0 +1,201 @@ +library angular.template_cache_generator; + +import 'dart:io'; +import 'dart:async'; +import 'dart:collection'; + +import 'package:analyzer/src/generated/ast.dart'; +import 'source_crawler_impl.dart'; + +const String PACKAGE_PREFIX = 'package:'; +const String DART_PACKAGE_PREFIX = 'dart:'; + +String fileHeader(String library) => '''// GENERATED, DO NOT EDIT! +library ${library}; + +import 'package:angular/angular.dart'; + +primeTemplateCache(TemplateCache tc) { +'''; + +const String FILE_FOOTER = '}'; +const SYSTEM_PACKAGE_ROOT = '%SYSTEM_PACKAGE_ROOT%'; + +main(args) { + if (args.length < 4) { + print('Usage: templace_cache_generator path_to_entry_point output ' + 'package_root1,package_root2,...|$SYSTEM_PACKAGE_ROOT ' + 'patternUrl1,rewriteTo1;patternUrl2,rewriteTo2 ' + 'blacklistClass1,blacklistClass2'); + exit(1); + } + + var entryPoint = args[0]; + var output = args[1]; + var outputLibrary = args[2]; + var packageRoots = args[3] == SYSTEM_PACKAGE_ROOT ? + [Platform.packageRoot] : args[3].split(','); + Map urlRewriters = parseUrlRemapping(args[4]); + Set blacklistedClasses = (args.length > 5) + ? new Set.from(args[5].split(',')) + : new Set(); + + print('entryPoint: $entryPoint'); + print('output: $output'); + print('outputLibrary: $outputLibrary'); + print('packageRoots: $packageRoots'); + print('url rewritters: ' + args[4]); + print('blacklistedClasses: ' + blacklistedClasses.join(', ')); + + + Map templates = {}; + + var c = new SourceCrawlerImpl(packageRoots); + var visitor = + new TemplateCollectingVisitor(templates, blacklistedClasses, c); + c.crawl(entryPoint, (CompilationUnit compilationUnit) => + visitor(compilationUnit)); + + var sink = new File(output).openWrite(); + return printTemplateCache( + templates, urlRewriters, outputLibrary, sink).then((_) { + return sink.flush(); + }); +} + +Map parseUrlRemapping(String argument) { + Map result = new LinkedHashMap(); + if (argument.isEmpty) { + return result; + } + + argument.split(";").forEach((String pair) { + List remapping = pair.split(","); + result[new RegExp(remapping[0])] = remapping[1]; + }); + return result; +} + +printTemplateCache(Map templateKeyMap, + Map urlRewriters, + String outputLibrary, + IOSink outSink) { + + outSink.write(fileHeader(outputLibrary)); + + List reads = []; + templateKeyMap.forEach((uri, templateFile) { + reads.add(new File(templateFile).readAsString().then((fileStr) { + fileStr = fileStr.replaceAll('"""', r'\"\"\"'); + String resultUri = uri; + urlRewriters.forEach((regexp, replacement) { + resultUri = resultUri.replaceFirst(regexp, replacement); + }); + outSink.write( + 'tc.put("$resultUri", new HttpResponse(200, r"""$fileStr"""));\n'); + })); + }); + + // Wait until all templates files are processed. + return Future.wait(reads).then((_) { + outSink.write(FILE_FOOTER); + }); +} + +class TemplateCollectingVisitor { + Map templates; + Set blacklistedClasses; + SourceCrawlerImpl sourceCrawlerImpl; + + TemplateCollectingVisitor(this.templates, this.blacklistedClasses, + this.sourceCrawlerImpl); + + call(CompilationUnit cu) { + cu.declarations.forEach((CompilationUnitMember declaration) { + // We only care about classes. + if (declaration is! ClassDeclaration) return; + ClassDeclaration clazz = declaration; + List cacheUris = []; + bool cache = true; + clazz.metadata.forEach((Annotation ann) { + if (ann.arguments == null) return; // Ignore non-class annotations. + // TODO(tsander): Add library name as class name could conflict. + if (blacklistedClasses.contains(clazz.name.name)) return; + + switch (ann.name.name) { + case 'NgComponent': + extractNgComponentMetadata(ann, cacheUris); break; + case 'NgTemplateCache': + cache = extractNgTemplateCache(ann, cacheUris); break; + } + }); + if (cache && cacheUris.isNotEmpty) { + cacheUris.forEach((uri) => storeUriAsset(uri)); + } + }); + } + + void extractNgComponentMetadata(Annotation ann, List cacheUris) { + ann.arguments.arguments.forEach((Expression arg) { + if (arg is NamedExpression) { + NamedExpression namedArg = arg; + var paramName = namedArg.name.label.name; + if (paramName == 'templateUrl' || paramName == 'cssUrl') { + cacheUris.add(assertString(namedArg.expression).stringValue); + } + } + }); + } + + bool extractNgTemplateCache( + Annotation ann, List cacheUris) { + bool cache = true; + ann.arguments.arguments.forEach((Expression arg) { + if (arg is NamedExpression) { + NamedExpression namedArg = arg; + var paramName = namedArg.name.label.name; + if (paramName == 'preCacheUrls') { + assertList(namedArg.expression).elements + .forEach((expression) => + cacheUris.add(assertString(expression).stringValue)); + } + if (paramName == 'cache') { + cache = assertBoolean(namedArg.expression).value; + } + } + }); + return cache; + } + + void storeUriAsset(String uri) { + String assetFileLocation = + uri.startsWith('package:') ? + sourceCrawlerImpl.resolvePackagePath(uri) : uri; + if (assetFileLocation == null) { + print("Could not find asset for uri: $uri"); + } else { + templates[uri] = assetFileLocation; + } + } + + BooleanLiteral assertBoolean(Expression key) { + if (key is! BooleanLiteral) { + throw 'must be a boolean literal: ${key.runtimeType}'; + } + return key; + } + + ListLiteral assertList(Expression key) { + if (key is! ListLiteral) { + throw 'must be a list literal: ${key.runtimeType}'; + } + return key; + } + + StringLiteral assertString(Expression key) { + if (key is! StringLiteral) { + throw 'must be a string literal: ${key.runtimeType}'; + } + return key; + } +} diff --git a/test/io/all.dart b/test/io/all.dart index 14568a880..27c1c5959 100644 --- a/test/io/all.dart +++ b/test/io/all.dart @@ -2,8 +2,10 @@ library all_io_tests; import 'source_metadata_extractor_spec.dart' as source_metadata_extractor_spec; import 'expression_extractor_spec.dart' as expression_extractor_spec; +import 'template_cache_generator_spec.dart' as template_cache_generator_spec; main() { source_metadata_extractor_spec.main(); expression_extractor_spec.main(); + template_cache_generator_spec.main(); } \ No newline at end of file diff --git a/test/io/template_cache_generator_spec.dart b/test/io/template_cache_generator_spec.dart new file mode 100644 index 000000000..1b46ae90c --- /dev/null +++ b/test/io/template_cache_generator_spec.dart @@ -0,0 +1,36 @@ +library ng.tool.template_cache_generator_spec; + +import 'dart:async'; +import 'dart:io'; + +import 'package:angular/tools/template_cache_generator.dart' as generator; +import '../jasmine_syntax.dart'; +import 'package:unittest/unittest.dart'; + +main() => describe('template_cache_generator', () { + it('should correctly generate the templates cache file', () { + var tmpDir = Directory.systemTemp.createTempSync(); + Future flush; + try { + flush = generator.main(['test/io/test_files/templates/main.dart', + '${tmpDir.path}/generated.dart', 'generated', '%SYSTEM_PACKAGE_ROOT%', + 'test/io/test_files,rewritten', 'MyComponent3']); + } catch(_) { + tmpDir.deleteSync(recursive: true); + rethrow; + } + return flush.then((_) { + expect(new File('${tmpDir.path}/generated.dart').readAsStringSync(), + '// GENERATED, DO NOT EDIT!\n' + 'library generated;\n' + '\n' + 'import \'package:angular/angular.dart\';\n' + '\n' + 'primeTemplateCache(TemplateCache tc) {\n' + 'tc.put("rewritten/templates/main.html", new HttpResponse(200, r"""Hello World!"""));\n' + '}'); + }).whenComplete(() { + tmpDir.deleteSync(recursive: true); + }); + }); +}); diff --git a/test/io/test_files/templates/dont.html b/test/io/test_files/templates/dont.html new file mode 100644 index 000000000..3348e2b23 --- /dev/null +++ b/test/io/test_files/templates/dont.html @@ -0,0 +1 @@ +If you find me in the templates cache then it's broken. :( \ No newline at end of file diff --git a/test/io/test_files/templates/main.dart b/test/io/test_files/templates/main.dart new file mode 100644 index 000000000..f394c4cd2 --- /dev/null +++ b/test/io/test_files/templates/main.dart @@ -0,0 +1,29 @@ +library test_files.main; + +import 'package:angular/core/module.dart'; +import 'package:angular/tools/template_cache_annotation.dart'; + +@NgComponent( + selector: 'my-component', + templateUrl: 'test/io/test_files/templates/main.html' +) +@NgTemplateCache() +class MyComponent { +} + +@NgComponent( + selector: 'my-component2', + templateUrl: 'test/io/test_files/templates/dont.html' +) +@NgTemplateCache(cache: false) +class MyComponent2 { +} + + +@NgComponent( + selector: 'my-component3', + templateUrl: 'test/io/test_files/templates/dont.html' +) +@NgTemplateCache(cache: true) +class MyComponent3 { +} diff --git a/test/io/test_files/templates/main.html b/test/io/test_files/templates/main.html new file mode 100644 index 000000000..c57eff55e --- /dev/null +++ b/test/io/test_files/templates/main.html @@ -0,0 +1 @@ +Hello World! \ No newline at end of file