Skip to content

Commit 97d2456

Browse files
author
Tim Blasi
committed
feat(dart/transform): Inline templateUrl values
Modify DirectiveProcessor to inline `templateUrl` values to avoid making additional browser requests. Closes angular#1035
1 parent 655ed85 commit 97d2456

13 files changed

Lines changed: 294 additions & 24 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
library angular2.transform.common.async_string_writer;
2+
3+
import 'dart:async';
4+
import 'package:analyzer/src/generated/java_core.dart';
5+
6+
/// [PrintWriter] implementation that allows asynchronous printing via
7+
/// [asyncPrint] and [asyncToString]. See those methods for details.
8+
class AsyncStringWriter extends PrintWriter {
9+
/// All [Future]s we are currently waiting on.
10+
final List<Future<String>> _toAwait = <Future<String>>[];
11+
final List<StringBuffer> _bufs;
12+
StringBuffer _curr;
13+
int _asyncCount = 0;
14+
15+
AsyncStringWriter._(StringBuffer curr)
16+
: _curr = curr,
17+
_bufs = <StringBuffer>[curr];
18+
19+
AsyncStringWriter() : this._(new StringBuffer());
20+
21+
void print(x) {
22+
_curr.write(x);
23+
}
24+
25+
/// Adds the result of `futureText` to the writer at the current position
26+
/// in the string being built. If using this method, you must use
27+
/// [asyncToString] instead of [toString] to get the value of the writer or
28+
/// your string may not appear as expected.
29+
Future<String> asyncPrint(Future<String> futureText) {
30+
_semaphoreIncrement();
31+
var myBuf = new StringBuffer();
32+
_bufs.add(myBuf);
33+
_curr = new StringBuffer();
34+
_bufs.add(_curr);
35+
36+
var toAwait = futureText.then((val) {
37+
myBuf.write(val);
38+
return val;
39+
});
40+
_toAwait.add(toAwait);
41+
return toAwait.whenComplete(() {
42+
_semaphoreDecrementAndCleanup();
43+
_toAwait.remove(toAwait);
44+
});
45+
}
46+
47+
/// Waits for any values added via [asyncPrint] and returns the fully
48+
/// built string.
49+
Future<String> asyncToString() {
50+
_semaphoreIncrement();
51+
var bufLen = _bufs.length;
52+
return Future.wait(_toAwait).then((_) {
53+
return _bufs.sublist(0, bufLen).join('');
54+
}).whenComplete(_semaphoreDecrementAndCleanup);
55+
}
56+
57+
String toString() => _bufs.map((buf) => '$buf').join('(async gap)');
58+
59+
void _semaphoreIncrement() {
60+
++_asyncCount;
61+
}
62+
63+
void _semaphoreDecrementAndCleanup() {
64+
assert(_asyncCount > 0);
65+
66+
--_asyncCount;
67+
if (_asyncCount == 0) {
68+
_curr = _bufs[0];
69+
for (var i = 1; i < _bufs.length; ++i) {
70+
_curr.write('${_bufs[i]}');
71+
}
72+
_bufs.removeRange(1, _bufs.length);
73+
}
74+
}
75+
}

modules/angular2/src/transform/template_compiler/xhr_impl.dart renamed to modules/angular2/src/transform/common/xhr_impl.dart

File renamed without changes.

modules/angular2/src/transform/directive_processor/rewriter.dart

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
library angular2.transform.directive_processor.rewriter;
22

3+
import 'dart:async';
4+
35
import 'package:analyzer/analyzer.dart';
4-
import 'package:analyzer/src/generated/java_core.dart';
6+
import 'package:angular2/src/services/xhr.dart' show XHR;
57
import 'package:angular2/src/transform/common/annotation_matcher.dart';
8+
import 'package:angular2/src/transform/common/asset_reader.dart';
9+
import 'package:angular2/src/transform/common/async_string_writer.dart';
610
import 'package:angular2/src/transform/common/logging.dart';
711
import 'package:angular2/src/transform/common/names.dart';
12+
import 'package:angular2/src/transform/common/xhr_impl.dart';
813
import 'package:barback/barback.dart' show AssetId;
914
import 'package:path/path.dart' as path;
1015

@@ -17,20 +22,22 @@ import 'visitors.dart';
1722
/// If no Angular 2 `Directive`s are found in `code`, returns the empty
1823
/// string unless `forceGenerate` is true, in which case an empty ngDeps
1924
/// file is created.
20-
String createNgDeps(
21-
String code, AssetId assetId, AnnotationMatcher annotationMatcher) {
25+
Future<String> createNgDeps(AssetReader reader, AssetId assetId,
26+
AnnotationMatcher annotationMatcher) async {
2227
// TODO(kegluneq): Shortcut if we can determine that there are no
2328
// [Directive]s present, taking into account `export`s.
24-
var writer = new PrintStringWriter();
25-
var visitor = new CreateNgDepsVisitor(writer, assetId, annotationMatcher);
26-
parseCompilationUnit(code, name: assetId.toString()).accept(visitor);
27-
return '$writer';
29+
var writer = new AsyncStringWriter();
30+
var visitor = new CreateNgDepsVisitor(
31+
writer, assetId, new XhrImpl(reader, assetId), annotationMatcher);
32+
var code = await reader.readAsString(assetId);
33+
parseCompilationUnit(code, name: assetId.path).accept(visitor);
34+
return await writer.asyncToString();
2835
}
2936

3037
/// Visitor responsible for processing [CompilationUnit] and creating an
3138
/// associated .ng_deps.dart file.
3239
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
33-
final PrintWriter writer;
40+
final AsyncStringWriter writer;
3441
bool _foundNgDirectives = false;
3542
bool _wroteImport = false;
3643
final ToSourceVisitor _copyVisitor;
@@ -42,12 +49,13 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
4249
/// The assetId for the file which we are parsing.
4350
final AssetId assetId;
4451

45-
CreateNgDepsVisitor(PrintWriter writer, this.assetId, this._annotationMatcher)
52+
CreateNgDepsVisitor(
53+
AsyncStringWriter writer, this.assetId, XHR xhr, this._annotationMatcher)
4654
: writer = writer,
4755
_copyVisitor = new ToSourceVisitor(writer),
4856
_factoryVisitor = new FactoryTransformVisitor(writer),
4957
_paramsVisitor = new ParameterTransformVisitor(writer),
50-
_metaVisitor = new AnnotationsTransformVisitor(writer);
58+
_metaVisitor = new AnnotationsTransformVisitor(writer, xhr);
5159

5260
void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) {
5361
if (list == null) return;

modules/angular2/src/transform/directive_processor/transformer.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ library angular2.transform.directive_processor.transformer;
22

33
import 'dart:async';
44

5+
import 'package:angular2/src/transform/common/asset_reader.dart';
56
import 'package:angular2/src/transform/common/logging.dart' as log;
67
import 'package:angular2/src/transform/common/names.dart';
78
import 'package:angular2/src/transform/common/options.dart';
@@ -32,9 +33,9 @@ class DirectiveProcessor extends Transformer {
3233

3334
try {
3435
var asset = transform.primaryInput;
35-
var assetCode = await asset.readAsString();
36+
var reader = new AssetReader.fromTransform(transform);
3637
var ngDepsSrc =
37-
createNgDeps(assetCode, asset.id, options.annotationMatcher);
38+
await createNgDeps(reader, asset.id, options.annotationMatcher);
3839
if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
3940
var ngDepsAssetId =
4041
transform.primaryInput.id.changeExtension(DEPS_EXTENSION);

modules/angular2/src/transform/directive_processor/visitors.dart

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ library angular2.transform.directive_processor.visitors;
22

33
import 'package:analyzer/analyzer.dart';
44
import 'package:analyzer/src/generated/java_core.dart';
5+
import 'package:angular2/src/services/xhr.dart' show XHR;
6+
import 'package:angular2/src/transform/common/async_string_writer.dart';
57
import 'package:angular2/src/transform/common/logging.dart';
68

79
/// `ToSourceVisitor` designed to accept {@link ConstructorDeclaration} nodes.
@@ -200,11 +202,17 @@ class FactoryTransformVisitor extends _CtorTransformVisitor {
200202
}
201203
}
202204

205+
// TODO(kegluenq): Use pull #1772 to detect when available.
206+
bool _isViewAnnotation(Annotation node) => '${node.name}' == 'View';
207+
203208
/// ToSourceVisitor designed to print a `ClassDeclaration` node as a
204209
/// 'annotations' value for Angular2's `registerType` calls.
205210
class AnnotationsTransformVisitor extends ToSourceVisitor {
206-
final PrintWriter writer;
207-
AnnotationsTransformVisitor(PrintWriter writer)
211+
final AsyncStringWriter writer;
212+
final XHR _xhr;
213+
bool _processingView = false;
214+
215+
AnnotationsTransformVisitor(AsyncStringWriter writer, this._xhr)
208216
: this.writer = writer,
209217
super(writer);
210218

@@ -226,6 +234,7 @@ class AnnotationsTransformVisitor extends ToSourceVisitor {
226234
Object visitAnnotation(Annotation node) {
227235
writer.print('const ');
228236
if (node.name != null) {
237+
_processingView = _isViewAnnotation(node);
229238
node.name.accept(this);
230239
}
231240
if (node.constructorName != null) {
@@ -237,4 +246,25 @@ class AnnotationsTransformVisitor extends ToSourceVisitor {
237246
}
238247
return null;
239248
}
249+
250+
/// These correspond to the annotation parameters.
251+
@override
252+
Object visitNamedExpression(NamedExpression node) {
253+
// TODO(kegluneq): Remove this limitation.
254+
if (!_processingView ||
255+
node.name is! Label ||
256+
node.name.label is! SimpleIdentifier) {
257+
return super.visitNamedExpression(node);
258+
}
259+
var keyString = '${node.name.label}';
260+
if (keyString == 'templateUrl' && node.expression is SimpleStringLiteral) {
261+
var url = stringLiteralToString(node.expression);
262+
writer.print("template: r'''");
263+
writer.asyncPrint(_xhr.get(url));
264+
writer.print("'''");
265+
return null;
266+
} else {
267+
return super.visitNamedExpression(node);
268+
}
269+
}
240270
}

modules/angular2/src/transform/template_compiler/generator.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ import 'package:angular2/src/change_detection/parser/parser.dart' as ng;
77
import 'package:angular2/src/render/api.dart';
88
import 'package:angular2/src/render/dom/compiler/compiler.dart';
99
import 'package:angular2/src/render/dom/compiler/template_loader.dart';
10-
import "package:angular2/src/services/xhr.dart" show XHR;
10+
import 'package:angular2/src/services/xhr.dart' show XHR;
1111
import 'package:angular2/src/reflection/reflection.dart';
1212
import 'package:angular2/src/services/url_resolver.dart';
1313
import 'package:angular2/src/transform/common/asset_reader.dart';
1414
import 'package:angular2/src/transform/common/names.dart';
1515
import 'package:angular2/src/transform/common/property_utils.dart' as prop;
16+
import 'package:angular2/src/transform/common/xhr_impl.dart';
1617
import 'package:barback/barback.dart';
1718

1819
import 'compile_step_factory.dart';
1920
import 'recording_reflection_capabilities.dart';
2021
import 'view_definition_creator.dart';
21-
import 'xhr_impl.dart';
2222

2323
/// Reads the `.ng_deps.dart` file represented by `entryPoint` and parses any
2424
/// Angular 2 `View` annotations it declares to generate `getter`s,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
library angular2.test.transform.common.async_string_writer;
2+
3+
import 'dart:async';
4+
import 'package:angular2/src/transform/common/async_string_writer.dart';
5+
import 'package:guinness/guinness.dart';
6+
7+
void allTests() {
8+
it('should function as a basic Writer without async calls.', () {
9+
var writer = new AsyncStringWriter();
10+
writer.print('hello');
11+
expect('$writer').toEqual('hello');
12+
writer.println(', world');
13+
expect('$writer').toEqual('hello, world\n');
14+
});
15+
16+
it('should concatenate futures added with `asyncPrint`.', () async {
17+
var writer = new AsyncStringWriter();
18+
writer.print('hello');
19+
expect('$writer').toEqual('hello');
20+
writer.asyncPrint(new Future.value(', world.'));
21+
writer.print(' It is a beautiful day.');
22+
expect(await writer.asyncToString())
23+
.toEqual('hello, world. It is a beautiful day.');
24+
});
25+
26+
it('should concatenate multiple futures regardless of order.', () async {
27+
var completer1 = new Completer<String>();
28+
var completer2 = new Completer<String>();
29+
30+
var writer = new AsyncStringWriter();
31+
writer.print('hello');
32+
expect('$writer').toEqual('hello');
33+
writer.asyncPrint(completer1.future);
34+
writer.asyncPrint(completer2.future);
35+
36+
completer2.complete(' It is a beautiful day.');
37+
completer1.complete(', world.');
38+
39+
expect(await writer.asyncToString())
40+
.toEqual('hello, world. It is a beautiful day.');
41+
});
42+
43+
it('should allow multiple "rounds" of `asyncPrint`.', () async {
44+
var writer = new AsyncStringWriter();
45+
writer.print('hello');
46+
expect('$writer').toEqual('hello');
47+
writer.asyncPrint(new Future.value(', world.'));
48+
expect(await writer.asyncToString()).toEqual('hello, world.');
49+
50+
writer.asyncPrint(new Future.value(' It is '));
51+
writer.asyncPrint(new Future.value('a beautiful '));
52+
writer.asyncPrint(new Future.value('day.'));
53+
54+
expect(await writer.asyncToString())
55+
.toEqual('hello, world. It is a beautiful day.');
56+
});
57+
58+
it('should handle calls to async methods while waiting.', () {
59+
var completer1 = new Completer<String>();
60+
var completer2 = new Completer<String>();
61+
62+
var writer = new AsyncStringWriter();
63+
writer.print('hello');
64+
expect('$writer').toEqual('hello');
65+
66+
writer.asyncPrint(completer1.future);
67+
var f1 = writer.asyncToString().then((result) {
68+
expect(result).toEqual('hello, world.');
69+
});
70+
71+
writer.asyncPrint(completer2.future);
72+
var f2 = writer.asyncToString().then((result) {
73+
expect(result).toEqual('hello, world. It is a beautiful day.');
74+
});
75+
76+
completer1.complete(', world.');
77+
completer2.complete(' It is a beautiful day.');
78+
79+
return Future.wait([f1, f2]);
80+
});
81+
82+
it('should handle calls to async methods that complete in reverse '
83+
'order while waiting.', () {
84+
var completer1 = new Completer<String>();
85+
var completer2 = new Completer<String>();
86+
87+
var writer = new AsyncStringWriter();
88+
writer.print('hello');
89+
expect('$writer').toEqual('hello');
90+
91+
writer.asyncPrint(completer1.future);
92+
var f1 = writer.asyncToString().then((result) {
93+
expect(result).toEqual('hello, world.');
94+
});
95+
96+
writer.asyncPrint(completer2.future);
97+
var f2 = writer.asyncToString().then((result) {
98+
expect(result).toEqual('hello, world. It is a beautiful day.');
99+
});
100+
101+
completer2.complete(' It is a beautiful day.');
102+
completer1.complete(', world.');
103+
104+
return Future.wait([f1, f2]);
105+
});
106+
}

modules/angular2/test/transform/common/read_file.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,29 @@ String readFile(String path) {
1919
}
2020

2121
class TestAssetReader implements AssetReader {
22-
Future<String> readAsString(AssetId id, {Encoding encoding}) =>
23-
new Future.value(readFile(id.path));
22+
/// This allows "faking"
23+
final Map<AssetId, String> _overrideAssets = <AssetId, String>{};
24+
25+
Future<String> readAsString(AssetId id, {Encoding encoding}) {
26+
if (_overrideAssets.containsKey(id)) {
27+
return new Future.value(_overrideAssets[id]);
28+
} else {
29+
return new Future.value(readFile(id.path));
30+
}
31+
}
2432

2533
Future<bool> hasInput(AssetId id) {
26-
var exists = false;
34+
var exists = _overrideAssets.containsKey(id);
35+
if (exists) return new Future.value(true);
36+
2737
for (var myPath in [id.path, 'test/transform/${id.path}']) {
2838
var file = new File(myPath);
2939
exists = exists || file.existsSync();
3040
}
3141
return new Future.value(exists);
3242
}
43+
44+
void addAsset(AssetId id, String contents) {
45+
_overrideAssets[id] = contents;
46+
}
3347
}

0 commit comments

Comments
 (0)