X Tutup
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/angular2/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ dependencies:
logging: '>=0.9.0 <0.12.0'
observe: '^0.13.1'
protobuf: '^0.5.0'
quiver: '^0.21.4'
source_span: '^1.0.0'
stack_trace: '^1.1.1'
dev_dependencies:
code_transformers: '>=0.2.9+4 <0.4.0'
guinness: '^0.1.18'
quiver: '^0.21.4'
test: '^0.12.6'
transformers:
- angular2
Expand Down
272 changes: 144 additions & 128 deletions modules_dart/transform/lib/src/transform/deferred_rewriter/rewriter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,160 +4,176 @@ import 'dart:async';

import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/ast.dart';
import 'package:barback/barback.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:angular2/src/transform/common/url_resolver.dart';
import 'package:barback/barback.dart';
import 'package:quiver/iterables.dart' as it;

class Rewriter {
final AssetId _entryPoint;
final AssetReader _reader;
final _FindDeferredLibraries _visitor;
/// Rewrites `loadLibrary` calls to initialize libraries once loaded.
///
/// 1. Finds all the deferred library imports and loadLibrary invocations in
/// `_entryPoint`
/// 2. Removes any libraries that don't require angular codegen.
/// 3. For the remaining libraries, rewrites the import to the corresponding
/// `.template.dart` file.
/// 4. Chains a future to the `loadLibrary` call which 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<String> rewriteLibrary(AssetId entryPoint, AssetReader reader) async {
var code = await reader.readAsString(entryPoint);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this really count as part of the time?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you could log it as a separate named operation if you want to keep it separate. I think its important to track this time somewhere though.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline - I like this idea but will tackle it in a separate PR.


return logElapsedAsync(() async {
// If we can determine there are no deferred libraries, avoid additional
// parsing the entire file and bail early.
var onlyDirectives = parseDirectives(code, name: entryPoint.path);
if (onlyDirectives == null) {
log.fine('No directives parsed, bailing early.', asset: entryPoint);
return null;
}

Rewriter(AssetId entryPoint, AssetReader reader)
: _entryPoint = entryPoint,
_reader = reader,
_visitor = new _FindDeferredLibraries(reader, entryPoint);
final importVisitor = new _FindDeferredLibraries(reader, entryPoint);
onlyDirectives.directives.accept(importVisitor);

/// Rewrites `loadLibrary` calls to initialize libraries once loaded.
///
/// 1. Finds all the deferred library imports and loadLibrary invocations in
/// `_entryPoint`
/// 2. Removes any libraries that don't require angular codegen.
/// 3. For the remaining libraries, rewrites the import to the corresponding
/// `.template.dart` file.
/// 4. Chains a future to the `loadLibrary` call which 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<String> rewrite() async {
var code = await _reader.readAsString(_entryPoint);

// If we can determine there are no deferred libraries, avoid parsing the
// entire file and bail early.
var onlyDirectives = parseDirectives(code, name: _entryPoint.path);
if (onlyDirectives == null) return null;
onlyDirectives.directives.accept(_visitor);
if (_visitor.deferredImports.isEmpty) return null;

var node = parseCompilationUnit(code, name: _entryPoint.path);
if (node == null) return null;

return logElapsedAsync(() async {
node.declarations.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) {
// Write from where we left off until the start of the import uri.
buf.write(code.substring(lastIdx, node.uri.offset));
// Rewrite the uri to be that of the generated file.
buf.write("'${toTemplateExtension('${node.uri.stringValue}')}'");
// Update the last index we've processed.
return node.uri.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';
}, operationName: 'rewriteDeferredLibraries', assetId: _entryPoint);
}
// Get imports that need rewriting.
final deferredImports = await importVisitor.process();
if (deferredImports.isEmpty) {
log.fine('There are no deferred library imports that need rewriting.',
asset: entryPoint);
return null;
}

var node = parseCompilationUnit(code, name: entryPoint.path);
if (node == null) {
log.fine('No declarations parsed, bailing early.', asset: entryPoint);
return null;
}

final declarationsVisitor = new _FindLoadLibraryCalls(deferredImports);
node.declarations.accept(declarationsVisitor);

// Get libraries that need rewriting.
if (declarationsVisitor.loadLibraryInvocations.isEmpty) {
log.fine(
'There are no loadLibrary invocations that need to be rewritten.',
asset: entryPoint);
return null;
}

return _rewriteLibrary(
code, deferredImports, declarationsVisitor.loadLibraryInvocations);
}, operationName: 'rewriteDeferredLibraries', assetId: entryPoint);
}

/// Rewrites the original [code] to initialize deferred libraries prior to use.
///
/// Note: This method may modify the order of [imports] and [loadLibCalls].
String _rewriteLibrary(String code, List<ImportDirective> imports,
List<MethodInvocation> loadLibCalls) {
/// Compares two [AstNode]s by position in the source code.
var _compareNodes = (AstNode a, AstNode b) => a.offset - b.offset;

// Necessary for indexes into [code] to function.
imports.sort(_compareNodes);
loadLibCalls.sort(_compareNodes);

var buf = new StringBuffer();
var idx = imports.fold(0, (int lastIdx, ImportDirective node) {
// Write from where we left off until the start of the import uri.
buf.write(code.substring(lastIdx, node.uri.offset));
// Rewrite the uri to be that of the generated file.
buf.write("'${toTemplateExtension('${node.uri.stringValue}')}'");
// Update the last index we've processed.
return node.uri.end;
});

idx = loadLibCalls.fold(idx, (int lastIdx, MethodInvocation node) {
buf.write(code.substring(lastIdx, node.offset));
var prefix = (node.realTarget as SimpleIdentifier).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 generated file.
class _FindDeferredLibraries extends Object with RecursiveAstVisitor<Object> {
var deferredImports = new List<ImportDirective>();
var loadLibraryInvocations = new List<MethodInvocation>();
/// Finds all `deferred` [ImportDirectives]s in an Ast that require init.
///
/// Use this to visit all [ImportDirective]s, then call [process] to get only
/// those [ImportDirective]s which are `deferred` and need Angular 2
/// initialization before use.
class _FindDeferredLibraries extends Object with SimpleAstVisitor<Object> {
final _deferredImports = <ImportDirective>[];
final _urlResolver = const TransformerUrlResolver();

final AssetReader _reader;
final AssetId _entryPoint;
final _urlResolver = const TransformerUrlResolver();
final String _entryPointUri;

_FindDeferredLibraries(this._reader, this._entryPoint);
_FindDeferredLibraries(this._reader, AssetId entryPoint)
: _entryPoint = entryPoint,
_entryPointUri = toAssetUri(entryPoint);

@override
Object visitImportDirective(ImportDirective node) {
if (node.deferredKeyword != null) {
deferredImports.add(node);
_deferredImports.add(node);
}
return null;
}

@override
Object visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name == 'loadLibrary') {
loadLibraryInvocations.add(node);
}
return super.visitMethodInvocation(node);
/// Gets the [AssetId] for the .ng_meta.json file associated with [import].
AssetId _getAssociatedMetaAsset(ImportDirective import) {
final importUri = stringLiteralToString(import.uri);
final associatedMetaUri = toMetaExtension(importUri);
return fromUri(_urlResolver.resolve(_entryPointUri, associatedMetaUri));
}

bool hasDeferredLibrariesToRewrite() {
if (deferredImports.isEmpty) {
log.fine('There are no deferred library imports.', asset: _entryPoint);
return false;
}
if (loadLibraryInvocations.isEmpty) {
log.fine(
'There are no loadLibrary invocations that need to be rewritten.',
asset: _entryPoint);
return false;
/// Gets a list of `deferred` [ImportDirective]s which need init.
///
/// Removes all [ImportDirective]s from [_deferredImports] without an
/// associated .ng_meta.json file.
Future<List<ImportDirective>> process() async {
// Parallel array with whether the input has an associated .ng_meta.json
// file.
final List<bool> hasInputs = await Future.wait(
_deferredImports.map(_getAssociatedMetaAsset).map(_reader.hasInput));

// Filter out any deferred imports that do not have an associated generated
// file.
// Iteration order is important!
for (var i = _deferredImports.length - 1; i >= 0; --i) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that using the reversed iterator for _deferredImports would be a bit cleaner here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, you need i for hasInputs so this makes sense.

if (!hasInputs[i]) {
_deferredImports.removeAt(i);
}
}
return true;
return _deferredImports;
}
}

// Remove all deferredImports that do not have an associated ng_meta file
// then remove all loadLibrary invocations that are not in the set of
// prefixes that are left.
Future cull() async {
var baseUri = toAssetUri(_entryPoint);
/// Finds all `loadLibrary` calls in the Ast that require init.
class _FindLoadLibraryCalls extends Object with RecursiveAstVisitor<Object> {
/// The prefixes used by all `deferred` [ImportDirective]s that need init.
final Set _deferredPrefixes;
final loadLibraryInvocations = <MethodInvocation>[];

// Determine whether a deferred import has an associated generated file.
var hasInputs = await Future.wait(deferredImports
.map((import) => stringLiteralToString(import.uri))
.map((uri) => toMetaExtension(uri))
.map((metaUri) => fromUri(_urlResolver.resolve(baseUri, metaUri)))
.map((asset) => _reader.hasInput(asset)));
_FindLoadLibraryCalls(List<ImportDirective> deferredImports)
: _deferredPrefixes =
new Set.from(deferredImports.map((import) => import.prefix.name));

// Filter out any deferred imports that do not have an associated generated
// file.
deferredImports = it.zip([deferredImports, hasInputs])
.where((importHasInput) => importHasInput[1])
.map((importHasInput) => importHasInput[0])
.toList();

// Find the set of prefixes which have associated generated files.
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 associated generated file.
loadLibraryInvocations = loadLibraryInvocations.where((library) {
var value = library.realTarget as SimpleIdentifier;
return prefixes.contains(value.name);
}).toList();

return;
@override
Object visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name == 'loadLibrary') {
var prefix = (node.realTarget as SimpleIdentifier).name;
if (_deferredPrefixes.contains(prefix)) {
loadLibraryInvocations.add(node);
}
}
// Important! Children could include more `loadLibrary` calls.
return super.visitMethodInvocation(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,5 @@ class DeferredRewriter extends AggregateTransformer implements LazyTransformer {
}

// Visible for testing
Future<String> rewriteDeferredLibraries(AssetReader reader, AssetId id) async {
var rewriter = new Rewriter(id, reader);
return await rewriter.rewrite();
}
Future<String> rewriteDeferredLibraries(AssetReader reader, AssetId id) =>
rewriteLibrary(id, reader);
X Tutup