diff --git a/modules/angular2/src/transform/common/options.dart b/modules/angular2/src/transform/common/options.dart index 9cc08b11e5..6fc9d772ed 100644 --- a/modules/angular2/src/transform/common/options.dart +++ b/modules/angular2/src/transform/common/options.dart @@ -3,9 +3,13 @@ library angular2.transform.common.options; import 'annotation_matcher.dart'; import 'mirror_mode.dart'; -const ENTRY_POINT_PARAM = 'entry_points'; -const REFLECTION_ENTRY_POINT_PARAM = 'reflection_entry_points'; +/// See `optimizationPhases` below for an explanation. +const DEFAULT_OPTIMIZATION_PHASES = 5; + const CUSTOM_ANNOTATIONS_PARAM = 'custom_annotations'; +const ENTRY_POINT_PARAM = 'entry_points'; +const OPTIMIZATION_PHASES_PARAM = 'optimization_phases'; +const REFLECTION_ENTRY_POINT_PARAM = 'reflection_entry_points'; /// Provides information necessary to transform an Angular2 app. class TransformerOptions { @@ -28,20 +32,32 @@ class TransformerOptions { /// The [AnnotationMatcher] which is used to identify angular annotations. final AnnotationMatcher annotationMatcher; + /// The number of phases to spend optimizing output size. + /// Each additional phase adds time to the transformation but may decrease + /// final output size. There is a limit beyond which this will no longer + /// decrease size, that is, setting this to 20 may not decrease size any + /// more than setting it to 10, but you will still pay an additional + /// penalty in transformation time. + /// The "correct" number of phases varies with the structure of the app. + final int optimizationPhases; + TransformerOptions._internal(this.entryPoints, this.reflectionEntryPoints, this.modeName, this.mirrorMode, this.initReflector, - this.annotationMatcher); + this.annotationMatcher, this.optimizationPhases); factory TransformerOptions(List entryPoints, {List reflectionEntryPoints, String modeName: 'release', MirrorMode mirrorMode: MirrorMode.none, bool initReflector: true, - List customAnnotationDescriptors: const []}) { + List customAnnotationDescriptors: const [], + int optimizationPhases: DEFAULT_OPTIMIZATION_PHASES}) { if (reflectionEntryPoints == null || reflectionEntryPoints.isEmpty) { reflectionEntryPoints = entryPoints; } var annotationMatcher = new AnnotationMatcher() ..addAll(customAnnotationDescriptors); + optimizationPhases = optimizationPhases.isNegative ? 0 : optimizationPhases; return new TransformerOptions._internal(entryPoints, reflectionEntryPoints, - modeName, mirrorMode, initReflector, annotationMatcher); + modeName, mirrorMode, initReflector, annotationMatcher, + optimizationPhases); } } diff --git a/modules/angular2/src/transform/common/options_reader.dart b/modules/angular2/src/transform/common/options_reader.dart index 38fb73bdf4..f352e66bd0 100644 --- a/modules/angular2/src/transform/common/options_reader.dart +++ b/modules/angular2/src/transform/common/options_reader.dart @@ -26,12 +26,15 @@ TransformerOptions parseBarbackSettings(BarbackSettings settings) { mirrorMode = MirrorMode.none; break; } + var optimizationPhases = _readInt(config, OPTIMIZATION_PHASES_PARAM, + defaultValue: DEFAULT_OPTIMIZATION_PHASES); return new TransformerOptions(entryPoints, reflectionEntryPoints: reflectionEntryPoints, modeName: settings.mode.name, mirrorMode: mirrorMode, initReflector: initReflector, - customAnnotationDescriptors: _readCustomAnnotations(config)); + customAnnotationDescriptors: _readCustomAnnotations(config), + optimizationPhases: optimizationPhases); } /// Cribbed from the polymer project. @@ -56,6 +59,18 @@ List _readFileList(Map config, String paramName) { return files; } +int _readInt(Map config, String paramName, {int defaultValue: null}) { + if (!config.containsKey(paramName)) return defaultValue; + var value = config[paramName]; + if (value is String) { + value = int.parse(value); + } + if (value is! int) { + throw new ArgumentError.value(value, paramName, 'Expected an integer'); + } + return value; +} + /// Parse the [CUSTOM_ANNOTATIONS_PARAM] options out of the transformer into /// [AnnotationDescriptor]s. List _readCustomAnnotations(Map config) { diff --git a/modules/angular2/src/transform/di_transformer.dart b/modules/angular2/src/transform/di_transformer.dart index 13d3bd1210..ae432c5bd3 100644 --- a/modules/angular2/src/transform/di_transformer.dart +++ b/modules/angular2/src/transform/di_transformer.dart @@ -22,9 +22,11 @@ class DiTransformerGroup extends TransformerGroup { factory DiTransformerGroup(TransformerOptions options) { var phases = [ [new ReflectionRemover(options)], - [new DirectiveProcessor(null)], - [new DirectiveLinker()] + [new DirectiveProcessor(null)] ]; + phases.addAll(new List.generate( + options.optimizationPhases, (_) => [new EmptyNgDepsRemover()])); + phases.add([new DirectiveLinker()]); return new DiTransformerGroup._(phases); } diff --git a/modules/angular2/src/transform/directive_linker/linker.dart b/modules/angular2/src/transform/directive_linker/linker.dart index 873b655946..9fe9a737f7 100644 --- a/modules/angular2/src/transform/directive_linker/linker.dart +++ b/modules/angular2/src/transform/directive_linker/linker.dart @@ -11,18 +11,56 @@ import 'package:barback/barback.dart'; import 'package:code_transformers/assets.dart'; import 'package:path/path.dart' as path; +/// Checks the `.ng_deps.dart` file represented by `entryPoint` and +/// determines whether it is necessary to the functioning of the Angular 2 +/// Dart app. +/// +/// An `.ng_deps.dart` file is not necessary if: +/// 1. It does not register any `@Injectable` types with the system. +/// 2. It does not import any libraries whose `.ng_deps.dart` files register +/// any `@Injectable` types with the system. +/// +/// Since `@Directive` and `@Component` inherit from `@Injectable`, we know +/// we will not miss processing any classes annotated with those tags. +Future isNecessary(AssetReader reader, AssetId entryPoint) async { + var parser = new Parser(reader); + NgDeps ngDeps = await parser.parse(entryPoint); + + if (ngDeps.registeredTypes.isNotEmpty) return true; + + // We do not register any @Injectables, do we call any dependencies? + var linkedDepsMap = + await _processNgImports(reader, entryPoint, _getSortedDeps(ngDeps)); + return linkedDepsMap.isNotEmpty; +} + +/// Modifies the `.ng_deps.dart` file represented by `entryPoint` to call its +/// dependencies associated `initReflector` methods. +/// +/// For example, if entry_point.ng_deps.dart imports dependency.dart, this +/// will check if dependency.ng_deps.dart exists. If it does, we add: +/// +/// ``` +/// import 'dependency.ng_deps.dart' as i0; +/// ... +/// void setupReflection(reflector) { +/// ... +/// i0.initReflector(reflector); +/// } +/// ``` Future linkNgDeps(AssetReader reader, AssetId entryPoint) async { var parser = new Parser(reader); NgDeps ngDeps = await parser.parse(entryPoint); + if (ngDeps == null) return null; - var allDeps = [] - ..addAll(ngDeps.imports) - ..addAll(ngDeps.exports) - ..sort((a, b) => a.end.compareTo(b.end)); + var allDeps = _getSortedDeps(ngDeps); var linkedDepsMap = await _processNgImports(reader, entryPoint, allDeps); - if (linkedDepsMap.isEmpty) return ngDeps.code; + if (linkedDepsMap.isEmpty) { + // We are not calling `initReflector` on any other libraries. + return ngDeps.code; + } var importBuf = new StringBuffer(); var declarationBuf = new StringBuffer(); @@ -49,6 +87,15 @@ Future linkNgDeps(AssetReader reader, AssetId entryPoint) async { '${code.substring(declarationSeamIdx)}'; } +/// All `import`s and `export`s in `ngDeps` sorted by order of appearance in +/// the file. +List _getSortedDeps(NgDeps ngDeps) { + return [] + ..addAll(ngDeps.imports) + ..addAll(ngDeps.exports) + ..sort((a, b) => a.end.compareTo(b.end)); +} + String _toDepsUri(String importUri) => '${path.withoutExtension(importUri)}${DEPS_EXTENSION}'; @@ -67,10 +114,8 @@ Future> _processNgImports(AssetReader reader, .where(_isNotDartDirective) .map((UriBasedDirective directive) { var ngDepsUri = _toDepsUri(stringLiteralToString(directive.uri)); - var ngDepsAsset = uriToAssetId(entryPoint, ngDepsUri, logger, - null /* - span */ - ); + var spanArg = null; + var ngDepsAsset = uriToAssetId(entryPoint, ngDepsUri, logger, spanArg); if (ngDepsAsset == entryPoint) return nullFuture; return reader.hasInput(ngDepsAsset).then((hasInput) { if (hasInput) { diff --git a/modules/angular2/src/transform/directive_linker/transformer.dart b/modules/angular2/src/transform/directive_linker/transformer.dart index 23a0a0d8c1..3c7bf9a7ab 100644 --- a/modules/angular2/src/transform/directive_linker/transformer.dart +++ b/modules/angular2/src/transform/directive_linker/transformer.dart @@ -29,8 +29,10 @@ class DirectiveLinker extends Transformer { var assetId = transform.primaryInput.id; var assetPath = assetId.path; var transformedCode = await linkNgDeps(reader, assetId); - var formattedCode = formatter.format(transformedCode, uri: assetPath); - transform.addOutput(new Asset.fromString(assetId, formattedCode)); + if (transformedCode != null) { + var formattedCode = formatter.format(transformedCode, uri: assetPath); + transform.addOutput(new Asset.fromString(assetId, formattedCode)); + } } catch (ex, stackTrace) { log.logger.error('Linking ng directives failed.\n' 'Exception: $ex\n' @@ -39,3 +41,29 @@ class DirectiveLinker extends Transformer { return null; } } + +/// Transformer responsible for removing unnecessary `.ng_deps.dart` files +/// created by {@link DirectiveProcessor}. +class EmptyNgDepsRemover extends Transformer { + EmptyNgDepsRemover(); + + @override + bool isPrimary(AssetId id) => id.path.endsWith(DEPS_EXTENSION); + + @override + Future apply(Transform transform) async { + log.init(transform); + + try { + var reader = new AssetReader.fromTransform(transform); + if (!(await isNecessary(reader, transform.primaryInput.id))) { + transform.consumePrimary(); + } + } catch (ex, stackTrace) { + log.logger.error('Removing unnecessary ng deps failed.\n' + 'Exception: $ex\n' + 'Stack Trace: $stackTrace'); + } + return null; + } +} diff --git a/modules/angular2/src/transform/directive_processor/rewriter.dart b/modules/angular2/src/transform/directive_processor/rewriter.dart index 142db35bee..f52031a68a 100644 --- a/modules/angular2/src/transform/directive_processor/rewriter.dart +++ b/modules/angular2/src/transform/directive_processor/rewriter.dart @@ -31,6 +31,12 @@ Future createNgDeps(AssetReader reader, AssetId assetId, writer, assetId, new XhrImpl(reader, assetId), annotationMatcher); var code = await reader.readAsString(assetId); parseCompilationUnit(code, name: assetId.path).accept(visitor); + + // If this library does not define an `@Injectable` and it does not import + // any libaries that could, then we do not need to generate a `.ng_deps + // .dart` file for it. + if (!visitor._foundNgInjectable && !visitor._usesNonLangLibs) return null; + return await writer.asyncToString(); } @@ -38,8 +44,13 @@ Future createNgDeps(AssetReader reader, AssetId assetId, /// associated .ng_deps.dart file. class CreateNgDepsVisitor extends Object with SimpleAstVisitor { final AsyncStringWriter writer; - bool _foundNgDirectives = false; - bool _wroteImport = false; + /// Whether an Angular 2 `Injectable` has been found. + bool _foundNgInjectable = false; + /// Whether this library `imports` or `exports` any non-'dart:' libraries. + bool _usesNonLangLibs = false; + /// Whether we have written an import of base file + /// (the file we are processing). + bool _wroteBaseLibImport = false; final ToSourceVisitor _copyVisitor; final FactoryTransformVisitor _factoryVisitor; final ParameterTransformVisitor _paramsVisitor; @@ -79,20 +90,27 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { /// Write the import to the file the .ng_deps.dart file is based on if it /// has not yet been written. void _maybeWriteImport() { - if (_wroteImport) return; - _wroteImport = true; + if (_wroteBaseLibImport) return; + _wroteBaseLibImport = true; writer.print('''import '${path.basename(assetId.path)}';'''); } + void _updateUsesNonLangLibs(UriBasedDirective directive) { + _usesNonLangLibs = _usesNonLangLibs || + !stringLiteralToString(directive.uri).startsWith('dart:'); + } + @override Object visitImportDirective(ImportDirective node) { _maybeWriteImport(); + _updateUsesNonLangLibs(node); return node.accept(_copyVisitor); } @override Object visitExportDirective(ExportDirective node) { _maybeWriteImport(); + _updateUsesNonLangLibs(node); return node.accept(_copyVisitor); } @@ -104,7 +122,7 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { } void _closeFunctionWrapper() { - if (_foundNgDirectives) { + if (_foundNgInjectable) { writer.print(';'); } writer.print('}'); @@ -153,10 +171,10 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor { var ctor = _getCtor(node); - if (!_foundNgDirectives) { + if (!_foundNgInjectable) { // The receiver for cascaded calls. writer.print(REFLECTOR_VAR_NAME); - _foundNgDirectives = true; + _foundNgInjectable = true; } writer.print('..registerType('); node.name.accept(this); diff --git a/modules/angular2/src/transform/transformer.dart b/modules/angular2/src/transform/transformer.dart index 966869a0ee..270444ffc1 100644 --- a/modules/angular2/src/transform/transformer.dart +++ b/modules/angular2/src/transform/transformer.dart @@ -24,11 +24,15 @@ class AngularTransformerGroup extends TransformerGroup { factory AngularTransformerGroup(TransformerOptions options) { var phases = [ [new ReflectionRemover(options)], - [new DirectiveProcessor(options)], + [new DirectiveProcessor(options)] + ]; + phases.addAll(new List.generate( + options.optimizationPhases, (_) => [new EmptyNgDepsRemover()])); + phases.addAll([ [new DirectiveLinker(), new DirectiveMetadataExtractor()], [new BindGenerator(options)], [new TemplateCompiler(options)] - ]; + ]); return new AngularTransformerGroup._(phases); } diff --git a/modules/angular2/test/transform/directive_processor/all_tests.dart b/modules/angular2/test/transform/directive_processor/all_tests.dart index 3a7cfbe983..1a777fe331 100644 --- a/modules/angular2/test/transform/directive_processor/all_tests.dart +++ b/modules/angular2/test/transform/directive_processor/all_tests.dart @@ -50,13 +50,18 @@ void _testNgDeps(String name, String inputPath, reader.addAsset(assetId, await reader.readAsString(inputId)); inputId = assetId; } - var annotationMatcher = new AnnotationMatcher()..addAll(customDescriptors); - var output = formatter - .format(await createNgDeps(reader, inputId, annotationMatcher)); var expectedPath = path.join(path.dirname(inputPath), 'expected', path.basename(inputPath).replaceFirst('.dart', '.ng_deps.dart')); var expectedId = _assetIdForPath(expectedPath); - expect(output).toEqual(await reader.readAsString(expectedId)); + + var annotationMatcher = new AnnotationMatcher()..addAll(customDescriptors); + var output = await createNgDeps(reader, inputId, annotationMatcher); + if (output == null) { + expect(await reader.hasInput(expectedId)).toBeFalse(); + } else { + expect(formatter.format(output)) + .toEqual(await reader.readAsString(expectedId)); + } }); } diff --git a/modules/angular2/test/transform/directive_processor/custom_metadata/expected/bad_soup.ng_deps.dart b/modules/angular2/test/transform/directive_processor/custom_metadata/expected/bad_soup.ng_deps.dart deleted file mode 100644 index c2970f51a6..0000000000 --- a/modules/angular2/test/transform/directive_processor/custom_metadata/expected/bad_soup.ng_deps.dart +++ /dev/null @@ -1,9 +0,0 @@ -library dinner.bad_soup.ng_deps.dart; - -import 'bad_soup.dart'; - -var _visited = false; -void initReflector(reflector) { - if (_visited) return; - _visited = true; -} diff --git a/modules/angular2/test/transform/integration/list_of_types_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/list_of_types_files/expected/bar.ng_deps.dart index bb384647e3..ea5db8c8ae 100644 --- a/modules/angular2/test/transform/integration/list_of_types_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/list_of_types_files/expected/bar.ng_deps.dart @@ -2,10 +2,7 @@ library bar.ng_deps.dart; import 'bar.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart'; -import 'package:angular2/src/core/annotations_impl/annotations.ng_deps.dart' - as i0; import 'foo.dart'; -import 'foo.ng_deps.dart' as i1; var _visited = false; void initReflector(reflector) { @@ -18,6 +15,4 @@ void initReflector(reflector) { 'annotations': const [const Component(componentServices: const [MyContext])] }); - i0.initReflector(reflector); - i1.initReflector(reflector); } diff --git a/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_deps.dart index a6616a9ac0..d9644279d0 100644 --- a/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/simple_annotation_files/expected/bar.ng_deps.dart @@ -2,8 +2,6 @@ library bar.ng_deps.dart; import 'bar.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart'; -import 'package:angular2/src/core/annotations_impl/annotations.ng_deps.dart' - as i0; var _visited = false; void initReflector(reflector) { @@ -15,5 +13,4 @@ void initReflector(reflector) { 'parameters': const [], 'annotations': const [const Component(selector: '[soup]')] }); - i0.initReflector(reflector); } diff --git a/modules/angular2/test/transform/integration/simple_annotation_files/expected/index.ng_deps.dart b/modules/angular2/test/transform/integration/simple_annotation_files/expected/index.ng_deps.dart index 32ef4dc0e2..69c78201a2 100644 --- a/modules/angular2/test/transform/integration/simple_annotation_files/expected/index.ng_deps.dart +++ b/modules/angular2/test/transform/integration/simple_annotation_files/expected/index.ng_deps.dart @@ -2,16 +2,14 @@ library web_foo.ng_deps.dart; import 'index.dart'; import 'package:angular2/src/core/application.dart'; -import 'package:angular2/src/core/application.ng_deps.dart' as i0; import 'package:angular2/src/reflection/reflection.dart'; import 'index.ng_deps.dart' as ngStaticInit0; import 'bar.dart'; -import 'bar.ng_deps.dart' as i1; +import 'bar.ng_deps.dart' as i0; var _visited = false; void initReflector(reflector) { if (_visited) return; _visited = true; i0.initReflector(reflector); - i1.initReflector(reflector); } diff --git a/modules/angular2/test/transform/integration/synthetic_ctor_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/synthetic_ctor_files/expected/bar.ng_deps.dart index a6616a9ac0..d9644279d0 100644 --- a/modules/angular2/test/transform/integration/synthetic_ctor_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/synthetic_ctor_files/expected/bar.ng_deps.dart @@ -2,8 +2,6 @@ library bar.ng_deps.dart; import 'bar.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart'; -import 'package:angular2/src/core/annotations_impl/annotations.ng_deps.dart' - as i0; var _visited = false; void initReflector(reflector) { @@ -15,5 +13,4 @@ void initReflector(reflector) { 'parameters': const [], 'annotations': const [const Component(selector: '[soup]')] }); - i0.initReflector(reflector); } diff --git a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart index d60dc80e2a..9dd8bbd021 100644 --- a/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/two_annotations_files/expected/bar.ng_deps.dart @@ -2,10 +2,7 @@ library bar.ng_deps.dart; import 'bar.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart'; -import 'package:angular2/src/core/annotations_impl/annotations.ng_deps.dart' - as i0; import 'package:angular2/src/core/annotations_impl/view.dart'; -import 'package:angular2/src/core/annotations_impl/view.ng_deps.dart' as i1; var _visited = false; void initReflector(reflector) { @@ -20,6 +17,4 @@ void initReflector(reflector) { const View(template: 'Salad') ] }); - i0.initReflector(reflector); - i1.initReflector(reflector); } diff --git a/modules/angular2/test/transform/integration/two_deps_files/expected/bar.ng_deps.dart b/modules/angular2/test/transform/integration/two_deps_files/expected/bar.ng_deps.dart index 2cc2d28912..33d29a8dac 100644 --- a/modules/angular2/test/transform/integration/two_deps_files/expected/bar.ng_deps.dart +++ b/modules/angular2/test/transform/integration/two_deps_files/expected/bar.ng_deps.dart @@ -2,10 +2,7 @@ library bar.ng_deps.dart; import 'bar.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart'; -import 'package:angular2/src/core/annotations_impl/annotations.ng_deps.dart' - as i0; import 'foo.dart' as prefix; -import 'foo.ng_deps.dart' as i1; var _visited = false; void initReflector(reflector) { @@ -18,6 +15,4 @@ void initReflector(reflector) { 'parameters': const [const [prefix.MyContext], const [String]], 'annotations': const [const Component(selector: 'soup')] }); - i0.initReflector(reflector); - i1.initReflector(reflector); }