library angular2.transform.deferred_rewriter.rewriter; import 'dart:async'; import 'package:analyzer/analyzer.dart'; import 'package:analyzer/src/generated/ast.dart'; import 'package:angular2/src/transform/common/asset_reader.dart'; import 'package:angular2/src/transform/common/logging.dart'; import 'package:angular2/src/transform/common/names.dart'; import 'package:barback/barback.dart'; import 'package:code_transformers/assets.dart'; import 'package:quiver/iterables.dart' as it; class Rewriter { final AssetId _entryPoint; final AssetReader _reader; Rewriter(this._entryPoint, this._reader); /// Rewrites the provided code by finding all the deferred library imports /// and loadLibrary invocations. Then it removes any libraries that don't /// require angular codegen. For the remaining libraries it rewrites the /// import to the corresponding ng_dep file, and chains a future which /// first initializes the library. /// /// To the extent possible, this method does not change line numbers or /// offsets in the provided code to facilitate debugging via source maps. Future rewrite() async { var code = await _reader.readAsString(_entryPoint); var node = parseCompilationUnit(code); if (node == null) return null; var visitor = new _FindDeferredLibraries(_reader, _entryPoint); node.accept(visitor); // Look to see if we found any deferred libraries if (!visitor.hasDeferredLibrariesToRewrite()) return null; // Remove any libraries that don't need angular codegen. await visitor.cull(); // Check again if there are any deferred libraries. if (!visitor.hasDeferredLibrariesToRewrite()) return null; var compare = (AstNode a, AstNode b) => a.offset - b.offset; visitor.deferredImports.sort(compare); visitor.loadLibraryInvocations.sort(compare); var buf = new StringBuffer(); var idx = visitor.deferredImports.fold(0, (int lastIdx, ImportDirective node) { buf.write(code.substring(lastIdx, node.offset)); var import = code.substring(node.offset, node.end); buf.write(import.replaceFirst('.dart', DEPS_EXTENSION)); return node.end; }); idx = visitor.loadLibraryInvocations.fold(idx, (int lastIdx, MethodInvocation node) { buf.write(code.substring(lastIdx, node.offset)); var value = node.realTarget as SimpleIdentifier; var prefix = value.name; // Chain a future that initializes the reflector. buf.write('$prefix.loadLibrary().then((_) {$prefix.initReflector();})'); return node.end; }); if (idx < code.length) buf.write(code.substring(idx)); return '$buf'; } } /// Visitor responsible for finding the deferred libraries that need angular /// codegen. Those are the libraries that are loaded deferred and have a /// corresponding ng_deps file. class _FindDeferredLibraries extends Object with RecursiveAstVisitor { var deferredImports = new List(); var loadLibraryInvocations = new List(); final AssetReader _reader; final AssetId _entryPoint; _FindDeferredLibraries(this._reader, this._entryPoint); @override Object visitImportDirective(ImportDirective node) { if (node.deferredKeyword != null) { deferredImports.add(node); } return null; } @override Object visitMethodInvocation(MethodInvocation node) { if (node.methodName.name == 'loadLibrary') { loadLibraryInvocations.add(node); } return super.visitMethodInvocation(node); } bool hasDeferredLibrariesToRewrite() { if (deferredImports.isEmpty) { logger.fine('There are no deferred library imports.'); return false; } if (loadLibraryInvocations.isEmpty) { logger.fine( 'There are no loadLibrary invocations that need to be rewritten.'); return false; } return true; } // Remove all deferredImports that do not have a ng_dep file // then remove all loadLibrary invocations that are not in the set of // prefixes that are left. Future cull() async { // Determine whether a deferred import has ng_deps. var hasInputs = await Future.wait(deferredImports .map((import) => stringLiteralToString(import.uri)) .map((uri) => toDepsExtension(uri)) .map((depsUri) => uriToAssetId(_entryPoint, depsUri, logger, null, errorOnAbsolute: false)) .map((asset) => _reader.hasInput(asset))); // Filter out any deferred imports that do not have ng_deps. deferredImports = it .zip([deferredImports, hasInputs]) .where((importHasInput) => importHasInput[1]) .map((importHasInput) => importHasInput[0]) .toList(); // Find the set of prefixes which have ng_deps. var prefixes = new Set.from(deferredImports.map((import) => import.prefix.name)); // Filters out any load library invocations where the prefix is not a known // library with ng_deps. loadLibraryInvocations = loadLibraryInvocations.where((library) { var value = library.realTarget as SimpleIdentifier; return prefixes.contains(value.name); }).toList(); return; } }