feat(dart/transform): Inline `templateUrl` values
Modify DirectiveProcessor to inline `templateUrl` values to avoid making additional browser requests. Closes #1035
This commit is contained in:
parent
655ed851f0
commit
97d24563f4
|
@ -0,0 +1,75 @@
|
|||
library angular2.transform.common.async_string_writer;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:analyzer/src/generated/java_core.dart';
|
||||
|
||||
/// [PrintWriter] implementation that allows asynchronous printing via
|
||||
/// [asyncPrint] and [asyncToString]. See those methods for details.
|
||||
class AsyncStringWriter extends PrintWriter {
|
||||
/// All [Future]s we are currently waiting on.
|
||||
final List<Future<String>> _toAwait = <Future<String>>[];
|
||||
final List<StringBuffer> _bufs;
|
||||
StringBuffer _curr;
|
||||
int _asyncCount = 0;
|
||||
|
||||
AsyncStringWriter._(StringBuffer curr)
|
||||
: _curr = curr,
|
||||
_bufs = <StringBuffer>[curr];
|
||||
|
||||
AsyncStringWriter() : this._(new StringBuffer());
|
||||
|
||||
void print(x) {
|
||||
_curr.write(x);
|
||||
}
|
||||
|
||||
/// Adds the result of `futureText` to the writer at the current position
|
||||
/// in the string being built. If using this method, you must use
|
||||
/// [asyncToString] instead of [toString] to get the value of the writer or
|
||||
/// your string may not appear as expected.
|
||||
Future<String> asyncPrint(Future<String> futureText) {
|
||||
_semaphoreIncrement();
|
||||
var myBuf = new StringBuffer();
|
||||
_bufs.add(myBuf);
|
||||
_curr = new StringBuffer();
|
||||
_bufs.add(_curr);
|
||||
|
||||
var toAwait = futureText.then((val) {
|
||||
myBuf.write(val);
|
||||
return val;
|
||||
});
|
||||
_toAwait.add(toAwait);
|
||||
return toAwait.whenComplete(() {
|
||||
_semaphoreDecrementAndCleanup();
|
||||
_toAwait.remove(toAwait);
|
||||
});
|
||||
}
|
||||
|
||||
/// Waits for any values added via [asyncPrint] and returns the fully
|
||||
/// built string.
|
||||
Future<String> asyncToString() {
|
||||
_semaphoreIncrement();
|
||||
var bufLen = _bufs.length;
|
||||
return Future.wait(_toAwait).then((_) {
|
||||
return _bufs.sublist(0, bufLen).join('');
|
||||
}).whenComplete(_semaphoreDecrementAndCleanup);
|
||||
}
|
||||
|
||||
String toString() => _bufs.map((buf) => '$buf').join('(async gap)');
|
||||
|
||||
void _semaphoreIncrement() {
|
||||
++_asyncCount;
|
||||
}
|
||||
|
||||
void _semaphoreDecrementAndCleanup() {
|
||||
assert(_asyncCount > 0);
|
||||
|
||||
--_asyncCount;
|
||||
if (_asyncCount == 0) {
|
||||
_curr = _bufs[0];
|
||||
for (var i = 1; i < _bufs.length; ++i) {
|
||||
_curr.write('${_bufs[i]}');
|
||||
}
|
||||
_bufs.removeRange(1, _bufs.length);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
library angular2.transform.directive_processor.rewriter;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:analyzer/src/generated/java_core.dart';
|
||||
import 'package:angular2/src/services/xhr.dart' show XHR;
|
||||
import 'package:angular2/src/transform/common/annotation_matcher.dart';
|
||||
import 'package:angular2/src/transform/common/asset_reader.dart';
|
||||
import 'package:angular2/src/transform/common/async_string_writer.dart';
|
||||
import 'package:angular2/src/transform/common/logging.dart';
|
||||
import 'package:angular2/src/transform/common/names.dart';
|
||||
import 'package:angular2/src/transform/common/xhr_impl.dart';
|
||||
import 'package:barback/barback.dart' show AssetId;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
|
@ -17,20 +22,22 @@ import 'visitors.dart';
|
|||
/// If no Angular 2 `Directive`s are found in `code`, returns the empty
|
||||
/// string unless `forceGenerate` is true, in which case an empty ngDeps
|
||||
/// file is created.
|
||||
String createNgDeps(
|
||||
String code, AssetId assetId, AnnotationMatcher annotationMatcher) {
|
||||
Future<String> createNgDeps(AssetReader reader, AssetId assetId,
|
||||
AnnotationMatcher annotationMatcher) async {
|
||||
// TODO(kegluneq): Shortcut if we can determine that there are no
|
||||
// [Directive]s present, taking into account `export`s.
|
||||
var writer = new PrintStringWriter();
|
||||
var visitor = new CreateNgDepsVisitor(writer, assetId, annotationMatcher);
|
||||
parseCompilationUnit(code, name: assetId.toString()).accept(visitor);
|
||||
return '$writer';
|
||||
var writer = new AsyncStringWriter();
|
||||
var visitor = new CreateNgDepsVisitor(
|
||||
writer, assetId, new XhrImpl(reader, assetId), annotationMatcher);
|
||||
var code = await reader.readAsString(assetId);
|
||||
parseCompilationUnit(code, name: assetId.path).accept(visitor);
|
||||
return await writer.asyncToString();
|
||||
}
|
||||
|
||||
/// Visitor responsible for processing [CompilationUnit] and creating an
|
||||
/// associated .ng_deps.dart file.
|
||||
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
|
||||
final PrintWriter writer;
|
||||
final AsyncStringWriter writer;
|
||||
bool _foundNgDirectives = false;
|
||||
bool _wroteImport = false;
|
||||
final ToSourceVisitor _copyVisitor;
|
||||
|
@ -42,12 +49,13 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
|
|||
/// The assetId for the file which we are parsing.
|
||||
final AssetId assetId;
|
||||
|
||||
CreateNgDepsVisitor(PrintWriter writer, this.assetId, this._annotationMatcher)
|
||||
CreateNgDepsVisitor(
|
||||
AsyncStringWriter writer, this.assetId, XHR xhr, this._annotationMatcher)
|
||||
: writer = writer,
|
||||
_copyVisitor = new ToSourceVisitor(writer),
|
||||
_factoryVisitor = new FactoryTransformVisitor(writer),
|
||||
_paramsVisitor = new ParameterTransformVisitor(writer),
|
||||
_metaVisitor = new AnnotationsTransformVisitor(writer);
|
||||
_metaVisitor = new AnnotationsTransformVisitor(writer, xhr);
|
||||
|
||||
void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) {
|
||||
if (list == null) return;
|
||||
|
|
|
@ -2,6 +2,7 @@ library angular2.transform.directive_processor.transformer;
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:angular2/src/transform/common/asset_reader.dart';
|
||||
import 'package:angular2/src/transform/common/logging.dart' as log;
|
||||
import 'package:angular2/src/transform/common/names.dart';
|
||||
import 'package:angular2/src/transform/common/options.dart';
|
||||
|
@ -32,9 +33,9 @@ class DirectiveProcessor extends Transformer {
|
|||
|
||||
try {
|
||||
var asset = transform.primaryInput;
|
||||
var assetCode = await asset.readAsString();
|
||||
var reader = new AssetReader.fromTransform(transform);
|
||||
var ngDepsSrc =
|
||||
createNgDeps(assetCode, asset.id, options.annotationMatcher);
|
||||
await createNgDeps(reader, asset.id, options.annotationMatcher);
|
||||
if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
|
||||
var ngDepsAssetId =
|
||||
transform.primaryInput.id.changeExtension(DEPS_EXTENSION);
|
||||
|
|
|
@ -2,6 +2,8 @@ library angular2.transform.directive_processor.visitors;
|
|||
|
||||
import 'package:analyzer/analyzer.dart';
|
||||
import 'package:analyzer/src/generated/java_core.dart';
|
||||
import 'package:angular2/src/services/xhr.dart' show XHR;
|
||||
import 'package:angular2/src/transform/common/async_string_writer.dart';
|
||||
import 'package:angular2/src/transform/common/logging.dart';
|
||||
|
||||
/// `ToSourceVisitor` designed to accept {@link ConstructorDeclaration} nodes.
|
||||
|
@ -200,11 +202,17 @@ class FactoryTransformVisitor extends _CtorTransformVisitor {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO(kegluenq): Use pull #1772 to detect when available.
|
||||
bool _isViewAnnotation(Annotation node) => '${node.name}' == 'View';
|
||||
|
||||
/// ToSourceVisitor designed to print a `ClassDeclaration` node as a
|
||||
/// 'annotations' value for Angular2's `registerType` calls.
|
||||
class AnnotationsTransformVisitor extends ToSourceVisitor {
|
||||
final PrintWriter writer;
|
||||
AnnotationsTransformVisitor(PrintWriter writer)
|
||||
final AsyncStringWriter writer;
|
||||
final XHR _xhr;
|
||||
bool _processingView = false;
|
||||
|
||||
AnnotationsTransformVisitor(AsyncStringWriter writer, this._xhr)
|
||||
: this.writer = writer,
|
||||
super(writer);
|
||||
|
||||
|
@ -226,6 +234,7 @@ class AnnotationsTransformVisitor extends ToSourceVisitor {
|
|||
Object visitAnnotation(Annotation node) {
|
||||
writer.print('const ');
|
||||
if (node.name != null) {
|
||||
_processingView = _isViewAnnotation(node);
|
||||
node.name.accept(this);
|
||||
}
|
||||
if (node.constructorName != null) {
|
||||
|
@ -237,4 +246,25 @@ class AnnotationsTransformVisitor extends ToSourceVisitor {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// These correspond to the annotation parameters.
|
||||
@override
|
||||
Object visitNamedExpression(NamedExpression node) {
|
||||
// TODO(kegluneq): Remove this limitation.
|
||||
if (!_processingView ||
|
||||
node.name is! Label ||
|
||||
node.name.label is! SimpleIdentifier) {
|
||||
return super.visitNamedExpression(node);
|
||||
}
|
||||
var keyString = '${node.name.label}';
|
||||
if (keyString == 'templateUrl' && node.expression is SimpleStringLiteral) {
|
||||
var url = stringLiteralToString(node.expression);
|
||||
writer.print("template: r'''");
|
||||
writer.asyncPrint(_xhr.get(url));
|
||||
writer.print("'''");
|
||||
return null;
|
||||
} else {
|
||||
return super.visitNamedExpression(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@ import 'package:angular2/src/change_detection/parser/parser.dart' as ng;
|
|||
import 'package:angular2/src/render/api.dart';
|
||||
import 'package:angular2/src/render/dom/compiler/compiler.dart';
|
||||
import 'package:angular2/src/render/dom/compiler/template_loader.dart';
|
||||
import "package:angular2/src/services/xhr.dart" show XHR;
|
||||
import 'package:angular2/src/services/xhr.dart' show XHR;
|
||||
import 'package:angular2/src/reflection/reflection.dart';
|
||||
import 'package:angular2/src/services/url_resolver.dart';
|
||||
import 'package:angular2/src/transform/common/asset_reader.dart';
|
||||
import 'package:angular2/src/transform/common/names.dart';
|
||||
import 'package:angular2/src/transform/common/property_utils.dart' as prop;
|
||||
import 'package:angular2/src/transform/common/xhr_impl.dart';
|
||||
import 'package:barback/barback.dart';
|
||||
|
||||
import 'compile_step_factory.dart';
|
||||
import 'recording_reflection_capabilities.dart';
|
||||
import 'view_definition_creator.dart';
|
||||
import 'xhr_impl.dart';
|
||||
|
||||
/// Reads the `.ng_deps.dart` file represented by `entryPoint` and parses any
|
||||
/// Angular 2 `View` annotations it declares to generate `getter`s,
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
library angular2.test.transform.common.async_string_writer;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:angular2/src/transform/common/async_string_writer.dart';
|
||||
import 'package:guinness/guinness.dart';
|
||||
|
||||
void allTests() {
|
||||
it('should function as a basic Writer without async calls.', () {
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
writer.println(', world');
|
||||
expect('$writer').toEqual('hello, world\n');
|
||||
});
|
||||
|
||||
it('should concatenate futures added with `asyncPrint`.', () async {
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
writer.asyncPrint(new Future.value(', world.'));
|
||||
writer.print(' It is a beautiful day.');
|
||||
expect(await writer.asyncToString())
|
||||
.toEqual('hello, world. It is a beautiful day.');
|
||||
});
|
||||
|
||||
it('should concatenate multiple futures regardless of order.', () async {
|
||||
var completer1 = new Completer<String>();
|
||||
var completer2 = new Completer<String>();
|
||||
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
writer.asyncPrint(completer1.future);
|
||||
writer.asyncPrint(completer2.future);
|
||||
|
||||
completer2.complete(' It is a beautiful day.');
|
||||
completer1.complete(', world.');
|
||||
|
||||
expect(await writer.asyncToString())
|
||||
.toEqual('hello, world. It is a beautiful day.');
|
||||
});
|
||||
|
||||
it('should allow multiple "rounds" of `asyncPrint`.', () async {
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
writer.asyncPrint(new Future.value(', world.'));
|
||||
expect(await writer.asyncToString()).toEqual('hello, world.');
|
||||
|
||||
writer.asyncPrint(new Future.value(' It is '));
|
||||
writer.asyncPrint(new Future.value('a beautiful '));
|
||||
writer.asyncPrint(new Future.value('day.'));
|
||||
|
||||
expect(await writer.asyncToString())
|
||||
.toEqual('hello, world. It is a beautiful day.');
|
||||
});
|
||||
|
||||
it('should handle calls to async methods while waiting.', () {
|
||||
var completer1 = new Completer<String>();
|
||||
var completer2 = new Completer<String>();
|
||||
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
|
||||
writer.asyncPrint(completer1.future);
|
||||
var f1 = writer.asyncToString().then((result) {
|
||||
expect(result).toEqual('hello, world.');
|
||||
});
|
||||
|
||||
writer.asyncPrint(completer2.future);
|
||||
var f2 = writer.asyncToString().then((result) {
|
||||
expect(result).toEqual('hello, world. It is a beautiful day.');
|
||||
});
|
||||
|
||||
completer1.complete(', world.');
|
||||
completer2.complete(' It is a beautiful day.');
|
||||
|
||||
return Future.wait([f1, f2]);
|
||||
});
|
||||
|
||||
it('should handle calls to async methods that complete in reverse '
|
||||
'order while waiting.', () {
|
||||
var completer1 = new Completer<String>();
|
||||
var completer2 = new Completer<String>();
|
||||
|
||||
var writer = new AsyncStringWriter();
|
||||
writer.print('hello');
|
||||
expect('$writer').toEqual('hello');
|
||||
|
||||
writer.asyncPrint(completer1.future);
|
||||
var f1 = writer.asyncToString().then((result) {
|
||||
expect(result).toEqual('hello, world.');
|
||||
});
|
||||
|
||||
writer.asyncPrint(completer2.future);
|
||||
var f2 = writer.asyncToString().then((result) {
|
||||
expect(result).toEqual('hello, world. It is a beautiful day.');
|
||||
});
|
||||
|
||||
completer2.complete(' It is a beautiful day.');
|
||||
completer1.complete(', world.');
|
||||
|
||||
return Future.wait([f1, f2]);
|
||||
});
|
||||
}
|
|
@ -19,15 +19,29 @@ String readFile(String path) {
|
|||
}
|
||||
|
||||
class TestAssetReader implements AssetReader {
|
||||
Future<String> readAsString(AssetId id, {Encoding encoding}) =>
|
||||
new Future.value(readFile(id.path));
|
||||
/// This allows "faking"
|
||||
final Map<AssetId, String> _overrideAssets = <AssetId, String>{};
|
||||
|
||||
Future<String> readAsString(AssetId id, {Encoding encoding}) {
|
||||
if (_overrideAssets.containsKey(id)) {
|
||||
return new Future.value(_overrideAssets[id]);
|
||||
} else {
|
||||
return new Future.value(readFile(id.path));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> hasInput(AssetId id) {
|
||||
var exists = false;
|
||||
var exists = _overrideAssets.containsKey(id);
|
||||
if (exists) return new Future.value(true);
|
||||
|
||||
for (var myPath in [id.path, 'test/transform/${id.path}']) {
|
||||
var file = new File(myPath);
|
||||
exists = exists || file.existsSync();
|
||||
}
|
||||
return new Future.value(exists);
|
||||
}
|
||||
|
||||
void addAsset(AssetId id, String contents) {
|
||||
_overrideAssets[id] = contents;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ library angular2.test.transform.directive_processor.all_tests;
|
|||
import 'package:barback/barback.dart';
|
||||
import 'package:angular2/src/transform/directive_processor/rewriter.dart';
|
||||
import 'package:angular2/src/transform/common/annotation_matcher.dart';
|
||||
import '../common/read_file.dart';
|
||||
import 'package:dart_style/dart_style.dart';
|
||||
import 'package:guinness/guinness.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import '../common/read_file.dart';
|
||||
|
||||
var formatter = new DartFormatter();
|
||||
|
||||
|
@ -36,6 +36,9 @@ void allTests() {
|
|||
customDescriptors: [
|
||||
const AnnotationDescriptor('Soup', 'package:soup/soup.dart', 'Component'),
|
||||
]);
|
||||
|
||||
_testNgDeps(
|
||||
'should inline `templateUrl` values.', 'url_expression_files/hello.dart');
|
||||
}
|
||||
|
||||
void _testNgDeps(String name, String inputPath,
|
||||
|
@ -43,11 +46,13 @@ void _testNgDeps(String name, String inputPath,
|
|||
it(name, () async {
|
||||
var inputId = _assetIdForPath(inputPath);
|
||||
var reader = new TestAssetReader();
|
||||
var input = await reader.readAsString(inputId);
|
||||
if (assetId != null) {
|
||||
reader.addAsset(assetId, await reader.readAsString(inputId));
|
||||
inputId = assetId;
|
||||
}
|
||||
var annotationMatcher = new AnnotationMatcher()..addAll(customDescriptors);
|
||||
var proxyInputId = assetId != null ? assetId : inputId;
|
||||
var output =
|
||||
formatter.format(createNgDeps(input, proxyInputId, annotationMatcher));
|
||||
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);
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
library examples.src.hello_world.index_common_dart.ng_deps.dart;
|
||||
|
||||
import 'hello.dart';
|
||||
import 'package:angular2/angular2.dart'
|
||||
show bootstrap, Component, Directive, View, NgElement;
|
||||
|
||||
var _visited = false;
|
||||
void initReflector(reflector) {
|
||||
if (_visited) return;
|
||||
_visited = true;
|
||||
reflector
|
||||
..registerType(HelloCmp, {
|
||||
'factory': () => new HelloCmp(),
|
||||
'parameters': const [],
|
||||
'annotations': const [
|
||||
const Component(selector: 'hello-app'),
|
||||
const View(template: r'''{{greeting}}''')
|
||||
]
|
||||
});
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
library examples.src.hello_world.index_common_dart;
|
||||
|
||||
import 'package:angular2/angular2.dart'
|
||||
show bootstrap, Component, Directive, View, NgElement;
|
||||
|
||||
@Component(selector: 'hello-app')
|
||||
@View(templateUrl: 'template.html')
|
||||
class HelloCmp {}
|
|
@ -0,0 +1 @@
|
|||
{{greeting}}
|
|
@ -4,6 +4,7 @@ import 'package:guinness/guinness.dart';
|
|||
import 'package:unittest/unittest.dart' hide expect;
|
||||
import 'package:unittest/vm_config.dart';
|
||||
|
||||
import 'common/async_string_writer_tests.dart' as asyncStringWriter;
|
||||
import 'bind_generator/all_tests.dart' as bindGenerator;
|
||||
import 'directive_linker/all_tests.dart' as directiveLinker;
|
||||
import 'directive_metadata_extractor/all_tests.dart' as directiveMeta;
|
||||
|
@ -14,6 +15,7 @@ import 'template_compiler/all_tests.dart' as templateCompiler;
|
|||
|
||||
main() {
|
||||
useVMConfiguration();
|
||||
describe('AsyncStringWriter', asyncStringWriter.allTests);
|
||||
describe('Bind Generator', bindGenerator.allTests);
|
||||
describe('Directive Linker', directiveLinker.allTests);
|
||||
describe('Directive Metadata Extractor', directiveMeta.allTests);
|
||||
|
|
Loading…
Reference in New Issue