feat(dart/transform): Support `part` directives

Allow users to split libraries using the `part` directive.

Closes #1817
This commit is contained in:
Tim Blasi 2015-08-04 16:50:31 -07:00
parent b6ee20846b
commit aa480fee72
13 changed files with 335 additions and 115 deletions

View File

@ -1,3 +1,5 @@
library angular2.src.core.compiler.directive_lifecycle_reflector;
import 'package:angular2/src/core/annotations_impl/annotations.dart'; import 'package:angular2/src/core/annotations_impl/annotations.dart';
import 'package:angular2/src/core/compiler/interfaces.dart'; import 'package:angular2/src/core/compiler/interfaces.dart';
import 'package:angular2/src/reflection/reflection.dart'; import 'package:angular2/src/reflection/reflection.dart';

View File

@ -100,7 +100,8 @@ class Expect extends gns.Expect {
void toThrowError([message = ""]) => toThrowWith(message: message); void toThrowError([message = ""]) => toThrowWith(message: message);
void toThrowErrorWith(message) => expectException(this.actual, message); void toThrowErrorWith(message) => expectException(this.actual, message);
void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future); void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future);
void toHaveCssClass(className) => gns.guinness.matchers.toBeTrue(DOM.hasClass(actual, className)); void toHaveCssClass(className) =>
gns.guinness.matchers.toBeTrue(DOM.hasClass(actual, className));
void toImplement(expected) => toBeA(expected); void toImplement(expected) => toBeA(expected);
void toBeNaN() => void toBeNaN() =>
gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0); gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0);
@ -139,7 +140,8 @@ class NotExpect extends gns.NotExpect {
void toEqual(expected) => toHaveSameProps(expected); void toEqual(expected) => toHaveSameProps(expected);
void toBePromise() => gns.guinness.matchers.toBeFalse(actual is Future); void toBePromise() => gns.guinness.matchers.toBeFalse(actual is Future);
void toHaveCssClass(className) => gns.guinness.matchers.toBeFalse(DOM.hasClass(actual, className)); void toHaveCssClass(className) =>
gns.guinness.matchers.toBeFalse(DOM.hasClass(actual, className));
void toBeNull() => gns.guinness.matchers.toBeFalse(actual == null); void toBeNull() => gns.guinness.matchers.toBeFalse(actual == null);
Function get _expect => gns.guinness.matchers.expect; Function get _expect => gns.guinness.matchers.expect;
} }

View File

@ -16,7 +16,7 @@ class AsyncStringWriter extends PrintWriter {
: _curr = curr, : _curr = curr,
_bufs = <StringBuffer>[curr]; _bufs = <StringBuffer>[curr];
AsyncStringWriter() : this._(new StringBuffer()); AsyncStringWriter([Object content = ""]) : this._(new StringBuffer(content));
void print(x) { void print(x) {
_curr.write(x); _curr.write(x);

View File

@ -3,6 +3,7 @@ library angular2.transform.directive_processor.rewriter;
import 'dart:async'; import 'dart:async';
import 'package:analyzer/analyzer.dart'; import 'package:analyzer/analyzer.dart';
import 'package:analyzer/src/generated/java_core.dart';
import 'package:angular2/src/render/xhr.dart' show XHR; import 'package:angular2/src/render/xhr.dart' show XHR;
import 'package:angular2/src/transform/common/annotation_matcher.dart'; import 'package:angular2/src/transform/common/annotation_matcher.dart';
import 'package:angular2/src/transform/common/asset_reader.dart'; import 'package:angular2/src/transform/common/asset_reader.dart';
@ -13,7 +14,9 @@ import 'package:angular2/src/transform/common/names.dart';
import 'package:angular2/src/transform/common/xhr_impl.dart'; import 'package:angular2/src/transform/common/xhr_impl.dart';
import 'package:angular2/src/transform/common/ng_meta.dart'; import 'package:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart' show AssetId; import 'package:barback/barback.dart' show AssetId;
import 'package:code_transformers/assets.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:source_span/source_span.dart';
import 'visitors.dart'; import 'visitors.dart';
@ -29,33 +32,233 @@ Future<String> createNgDeps(AssetReader reader, AssetId assetId,
{bool inlineViews}) async { {bool inlineViews}) async {
// TODO(kegluneq): Shortcut if we can determine that there are no // TODO(kegluneq): Shortcut if we can determine that there are no
// [Directive]s present, taking into account `export`s. // [Directive]s present, taking into account `export`s.
var code = await reader.readAsString(assetId);
var directivesVisitor = new _NgDepsDirectivesVisitor();
parseDirectives(code, name: assetId.path)
.directives
.accept(directivesVisitor);
// If this is part of another library, its contents will be processed by its
// parent, so it does not need its own `.ng_deps.dart` file.
if (directivesVisitor.isPart) return null;
var writer = new AsyncStringWriter(); var writer = new AsyncStringWriter();
var visitor = new CreateNgDepsVisitor( directivesVisitor.writeTo(writer, assetId);
writer,
writer
..println('var _visited = false;')
..println('void ${SETUP_METHOD_NAME}() {')
..println('if (_visited) return; _visited = true;');
var declarationsCode =
await _getAllDeclarations(reader, assetId, code, directivesVisitor);
var declarationsVisitor = new _NgDepsDeclarationsVisitor(
assetId, assetId,
writer,
new XhrImpl(reader, assetId), new XhrImpl(reader, assetId),
annotationMatcher, annotationMatcher,
_interfaceMatcher, _interfaceMatcher,
ngMeta, ngMeta,
inlineViews: inlineViews); inlineViews: inlineViews);
var code = await reader.readAsString(assetId); parseCompilationUnit(declarationsCode, name: '${assetId.path} and parts')
parseCompilationUnit(code, name: assetId.path).accept(visitor); .declarations
.accept(declarationsVisitor);
if (declarationsVisitor.shouldCreateNgDeps) {
writer.println(';');
}
writer.println('}');
// If this library does not define an `@Injectable` and it does not import if (!directivesVisitor.shouldCreateNgDeps &&
// any libaries that could, then we do not need to generate a `.ng_deps !declarationsVisitor.shouldCreateNgDeps) return null;
// .dart` file for it.
if (!visitor._foundNgInjectable && !visitor._usesNonLangLibs) return null;
return await writer.asyncToString(); return writer.asyncToString();
} }
InterfaceMatcher _interfaceMatcher = new InterfaceMatcher(); InterfaceMatcher _interfaceMatcher = new InterfaceMatcher();
/// Visitor responsible for processing [CompilationUnit] and creating an /// Processes `visitor.parts`, reading and appending their contents to the
/// associated .ng_deps.dart file. /// original `code`.
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> { /// Order of `part`s is preserved. That is, if in the main library we have
/// ```
/// library main;
///
/// part 'lib1.dart'
/// part 'lib2.dart'
/// ```
/// The output will first have the entirety of the original file, followed by
/// the contents of lib1.dart followed by the contents of lib2.dart.
Future<String> _getAllDeclarations(AssetReader reader, AssetId assetId,
String code, _NgDepsDirectivesVisitor visitor) {
if (visitor.parts.isEmpty) return new Future<String>.value(code);
var partsStart = visitor.parts.first.offset,
partsEnd = visitor.parts.last.end;
var asyncWriter = new AsyncStringWriter(code.substring(0, partsStart));
visitor.parts.forEach((partDirective) {
var uri = stringLiteralToString(partDirective.uri);
var partAssetId = uriToAssetId(assetId, uri, logger, null /* span */,
errorOnAbsolute: false);
asyncWriter.asyncPrint(reader.readAsString(partAssetId).then((partCode) {
if (partCode == null || partCode.isEmpty) {
logger.warning('Empty part at "${partDirective.uri}. Ignoring.',
asset: partAssetId);
return '';
}
// Remove any directives -- we just want declarations.
var parsedDirectives = parseDirectives(partCode, name: uri).directives;
return partCode.substring(parsedDirectives.last.end);
}).catchError((err, stackTrace) {
logger.warning(
'Failed while reading part at ${partDirective.uri}. Ignoring.\n'
'Error: $err\n'
'Stack Trace: $stackTrace',
asset: partAssetId,
span: new SourceFile(code, url: path.basename(assetId.path))
.span(partDirective.offset, partDirective.end));
}));
});
asyncWriter.print(code.substring(partsEnd));
return asyncWriter.asyncToString();
}
/// Visitor responsible for flattening directives passed to it.
/// Once this has visited an Ast, use [#writeTo] to write out the directives
/// for the .ng_deps.dart file. See [#writeTo] for details.
class _NgDepsDirectivesVisitor extends Object with SimpleAstVisitor<Object> {
/// Whether this library `imports` or `exports` any non-'dart:' libraries.
bool _usesNonLangLibs = false;
/// Whether the file we are processing is a part, that is, whether we have
/// visited a `part of` directive.
bool _isPart = false;
// TODO(kegluneq): Support an intermediate representation of NgDeps and use it
// instead of storing generated code.
LibraryDirective _library = null;
ScriptTag _scriptTag = null;
final List<NamespaceDirective> _importAndExports = <NamespaceDirective>[];
final List<PartDirective> _parts = <PartDirective>[];
bool get shouldCreateNgDeps {
// 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 (!_usesNonLangLibs) return false;
if (_isPart) return false;
return true;
}
bool get usesNonLangLibs => _usesNonLangLibs;
bool get isPart => _isPart;
/// In the order encountered in the source.
Iterable<PartDirective> get parts => _parts;
@override
Object visitScriptTag(ScriptTag node) {
_scriptTag = node;
return null;
}
@override
Object visitCompilationUnit(CompilationUnit node) {
node.directives.accept(this);
return null;
}
void _updateUsesNonLangLibs(UriBasedDirective directive) {
_usesNonLangLibs = _usesNonLangLibs ||
!stringLiteralToString(directive.uri).startsWith('dart:');
}
@override
Object visitImportDirective(ImportDirective node) {
_updateUsesNonLangLibs(node);
_importAndExports.add(node);
return null;
}
@override
Object visitExportDirective(ExportDirective node) {
_updateUsesNonLangLibs(node);
_importAndExports.add(node);
return null;
}
@override
Object visitLibraryDirective(LibraryDirective node) {
if (node != null) {
_library = node;
}
return null;
}
@override
Object visitPartDirective(PartDirective node) {
_parts.add(node);
return null;
}
@override
Object visitPartOfDirective(PartOfDirective node) {
_isPart = true;
return null;
}
/// Write the directives for the .ng_deps.dart for `processedFile` to
/// `writer`. The .ng_deps.dart file has the same directives as
/// `processedFile` with some exceptions (mentioned below).
void writeTo(PrintWriter writer, AssetId processedFile) {
var copyVisitor = new ToSourceVisitor(writer);
if (_scriptTag != null) {
_scriptTag.accept(copyVisitor);
writer.newLine();
}
if (_library != null && _library.name != null) {
writer.print('library ');
_library.name.accept(copyVisitor);
writer.println('$DEPS_EXTENSION;');
}
// We do not output [PartDirective]s, which would not be valid now that we
// have changed the library.
// We need to import & export the original file.
var origDartFile = path.basename(processedFile.path);
writer.println('''import '$origDartFile';''');
writer.println('''export '$origDartFile';''');
// Used to register reflective information.
writer.println("import '$_REFLECTOR_IMPORT' as $_REF_PREFIX;");
_importAndExports.forEach((node) {
if (node.isSynthetic) return;
// Ignore deferred imports here so as to not load the deferred libraries
// code in the current library causing much of the code to not be
// deferred. Instead `DeferredRewriter` will rewrite the code as to load
// `ng_deps` in a deferred way.
if (node is ImportDirective && node.deferredKeyword != null) return;
node.accept(copyVisitor);
});
}
}
/// Visitor responsible for visiting a file's [Declaration]s and outputting the
/// code necessary to register the file with the Angular 2 system.
class _NgDepsDeclarationsVisitor extends Object with SimpleAstVisitor<Object> {
final AsyncStringWriter writer; final AsyncStringWriter writer;
/// The file we are processing.
final AssetId assetId;
/// Output ngMeta information about aliases. /// Output ngMeta information about aliases.
// TODO(sigmund): add more to ngMeta. Currently this only contains aliasing // TODO(sigmund): add more to ngMeta. Currently this only contains aliasing
// information, but we could produce here all the metadata we need and avoid // information, but we could produce here all the metadata we need and avoid
@ -65,25 +268,26 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
/// Whether an Angular 2 `Injectable` has been found. /// Whether an Angular 2 `Injectable` has been found.
bool _foundNgInjectable = false; bool _foundNgInjectable = false;
/// Whether this library `imports` or `exports` any non-'dart:' libraries. /// Visitor that writes out code for AstNodes visited.
bool _usesNonLangLibs = false;
/// Whether we have written an import of base file
/// (the file we are processing).
bool _wroteBaseLibImport = false;
final ToSourceVisitor _copyVisitor; final ToSourceVisitor _copyVisitor;
final FactoryTransformVisitor _factoryVisitor; final FactoryTransformVisitor _factoryVisitor;
final ParameterTransformVisitor _paramsVisitor; final ParameterTransformVisitor _paramsVisitor;
final AnnotationsTransformVisitor _metaVisitor; final AnnotationsTransformVisitor _metaVisitor;
/// Responsible for testing whether [Annotation]s are those recognized by
/// Angular 2, for example `@Component`.
final AnnotationMatcher _annotationMatcher; final AnnotationMatcher _annotationMatcher;
/// Responsible for testing whether interfaces are recognized by Angular2,
/// for example `OnChange`.
final InterfaceMatcher _interfaceMatcher; final InterfaceMatcher _interfaceMatcher;
/// The assetId for the file which we are parsing. /// Used to fetch linked files.
final AssetId assetId; final XHR _xhr;
CreateNgDepsVisitor( _NgDepsDeclarationsVisitor(
AsyncStringWriter writer,
AssetId assetId, AssetId assetId,
AsyncStringWriter writer,
XHR xhr, XHR xhr,
AnnotationMatcher annotationMatcher, AnnotationMatcher annotationMatcher,
InterfaceMatcher interfaceMatcher, InterfaceMatcher interfaceMatcher,
@ -98,75 +302,10 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
inlineViews: inlineViews), inlineViews: inlineViews),
_annotationMatcher = annotationMatcher, _annotationMatcher = annotationMatcher,
_interfaceMatcher = interfaceMatcher, _interfaceMatcher = interfaceMatcher,
this.assetId = assetId; this.assetId = assetId,
_xhr = xhr;
void _visitNodeListWithSeparator(NodeList<AstNode> list, String separator) { bool get shouldCreateNgDeps => _foundNgInjectable;
if (list == null) return;
for (var i = 0, iLen = list.length; i < iLen; ++i) {
if (i != 0) {
writer.print(separator);
}
list[i].accept(this);
}
}
@override
Object visitCompilationUnit(CompilationUnit node) {
_visitNodeListWithSeparator(node.directives, " ");
_openFunctionWrapper();
_visitNodeListWithSeparator(node.declarations, " ");
_closeFunctionWrapper();
return null;
}
/// Write the import to the file the .ng_deps.dart file is based on if it
/// has not yet been written.
void _maybeWriteImport() {
if (_wroteBaseLibImport) return;
_wroteBaseLibImport = true;
var origDartFile = path.basename(assetId.path);
writer.print('''import '$origDartFile';''');
writer.print('''export '$origDartFile';''');
writer.print("import '$_REFLECTOR_IMPORT' as $_REF_PREFIX;");
}
void _updateUsesNonLangLibs(UriBasedDirective directive) {
_usesNonLangLibs = _usesNonLangLibs ||
!stringLiteralToString(directive.uri).startsWith('dart:');
}
@override
Object visitImportDirective(ImportDirective node) {
_maybeWriteImport();
_updateUsesNonLangLibs(node);
// Ignore deferred imports here so as to not load the deferred libraries
// code in the current library causing much of the code to not be
// deferred. Instead `DeferredRewriter` will rewrite the code as to load
// `ng_deps` in a deferred way.
if (node.deferredKeyword != null) return null;
return node.accept(_copyVisitor);
}
@override
Object visitExportDirective(ExportDirective node) {
_maybeWriteImport();
_updateUsesNonLangLibs(node);
return node.accept(_copyVisitor);
}
void _openFunctionWrapper() {
_maybeWriteImport();
writer.print('var _visited = false;'
'void ${SETUP_METHOD_NAME}() {'
'if (_visited) return; _visited = true;');
}
void _closeFunctionWrapper() {
if (_foundNgInjectable) {
writer.print(';');
}
writer.print('}');
}
ConstructorDeclaration _getCtor(ClassDeclaration node) { ConstructorDeclaration _getCtor(ClassDeclaration node) {
int numCtorsFound = 0; int numCtorsFound = 0;
@ -271,25 +410,6 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
return node.accept(_copyVisitor); return node.accept(_copyVisitor);
} }
@override
Object visitLibraryDirective(LibraryDirective node) {
if (node != null && node.name != null) {
writer.print('library ');
_nodeToSource(node.name);
writer.print('$DEPS_EXTENSION;');
}
return null;
}
@override
Object visitPartOfDirective(PartOfDirective node) {
// TODO(kegluneq): Consider importing [node.libraryName].
logger.warning('[${assetId}]: '
'Found `part of` directive while generating ${DEPS_EXTENSION} file, '
'Transform may fail due to missing imports in generated file.');
return null;
}
@override @override
Object visitPrefixedIdentifier(PrefixedIdentifier node) => Object visitPrefixedIdentifier(PrefixedIdentifier node) =>
_nodeToSource(node); _nodeToSource(node);

View File

@ -95,7 +95,8 @@ Map<String, dynamic> serializeKeyboardEvent(dynamic e) {
} }
// TODO(jteplitz602): #3374. See above. // TODO(jteplitz602): #3374. See above.
Map<String, dynamic> addTarget(dynamic e, Map<String, dynamic> serializedEvent) { Map<String, dynamic> addTarget(
dynamic e, Map<String, dynamic> serializedEvent) {
if (NODES_WITH_VALUE.contains(e.target.tagName.toLowerCase())) { if (NODES_WITH_VALUE.contains(e.target.tagName.toLowerCase())) {
serializedEvent['target'] = {'value': e.target.value}; serializedEvent['target'] = {'value': e.target.value};
if (e.target is InputElement) { if (e.target is InputElement) {

View File

@ -12,6 +12,7 @@ import 'package:code_transformers/messages/build_logger.dart';
import 'package:dart_style/dart_style.dart'; import 'package:dart_style/dart_style.dart';
import 'package:guinness/guinness.dart'; import 'package:guinness/guinness.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:source_span/source_span.dart';
import '../common/read_file.dart'; import '../common/read_file.dart';
var formatter = new DartFormatter(); var formatter = new DartFormatter();
@ -24,6 +25,14 @@ void allTests() {
_testProcessor('should preserve parameter annotations as const instances.', _testProcessor('should preserve parameter annotations as const instances.',
'parameter_metadata/soup.dart'); 'parameter_metadata/soup.dart');
_testProcessor('should handle `part` directives.', 'part_files/main.dart');
_testProcessor('should handle multiple `part` directives.',
'multiple_part_files/main.dart');
_testProcessor('should not generate .ng_deps.dart for `part` files.',
'part_files/part.dart');
_testProcessor('should recognize custom annotations with package: imports', _testProcessor('should recognize custom annotations with package: imports',
'custom_metadata/package_soup.dart', 'custom_metadata/package_soup.dart',
customDescriptors: [ customDescriptors: [
@ -166,8 +175,9 @@ void _testProcessor(String name, String inputPath,
if (output == null) { if (output == null) {
expect(await reader.hasInput(expectedNgDepsId)).toBeFalse(); expect(await reader.hasInput(expectedNgDepsId)).toBeFalse();
} else { } else {
var input = await reader.readAsString(expectedNgDepsId); var expectedOutput = await reader.readAsString(expectedNgDepsId);
expect(formatter.format(output)).toEqual(formatter.format(input)); expect(formatter.format(output))
.toEqual(formatter.format(expectedOutput));
} }
if (ngMeta.isEmpty) { if (ngMeta.isEmpty) {
expect(await reader.hasInput(expectedAliasesId)).toBeFalse(); expect(await reader.hasInput(expectedAliasesId)).toBeFalse();

View File

@ -0,0 +1,25 @@
library main.ng_deps.dart;
import 'main.dart';
export 'main.dart';
import 'package:angular2/src/reflection/reflection.dart' as _ngRef;
import 'package:angular2/src/core/annotations_impl/annotations.dart';
var _visited = false;
void initReflector() {
if (_visited) return;
_visited = true;
_ngRef.reflector
..registerType(
Part1Component,
new _ngRef.ReflectionInfo(const [const Component(selector: '[part1]')],
const [], () => new Part1Component()))
..registerType(
Part2Component,
new _ngRef.ReflectionInfo(const [const Component(selector: '[part2]')],
const [], () => new Part2Component()))
..registerType(
MainComponent,
new _ngRef.ReflectionInfo(const [const Component(selector: '[main]')],
const [], () => new MainComponent()));
}

View File

@ -0,0 +1,11 @@
library main;
import 'package:angular2/src/core/annotations_impl/annotations.dart';
part 'part1.dart';
part 'part2.dart';
@Component(selector: '[main]')
class MainComponent {
MainComponent();
}

View File

@ -0,0 +1,6 @@
part of main;
@Component(selector: '[part1]')
class Part1Component {
Part1Component();
}

View File

@ -0,0 +1,6 @@
part of main;
@Component(selector: '[part2]')
class Part2Component {
Part2Component();
}

View File

@ -0,0 +1,21 @@
library main.ng_deps.dart;
import 'main.dart';
export 'main.dart';
import 'package:angular2/src/reflection/reflection.dart' as _ngRef;
import 'package:angular2/src/core/annotations_impl/annotations.dart';
var _visited = false;
void initReflector() {
if (_visited) return;
_visited = true;
_ngRef.reflector
..registerType(
PartComponent,
new _ngRef.ReflectionInfo(const [const Component(selector: '[part]')],
const [], () => new PartComponent()))
..registerType(
MainComponent,
new _ngRef.ReflectionInfo(const [const Component(selector: '[main]')],
const [], () => new MainComponent()));
}

View File

@ -0,0 +1,10 @@
library main;
import 'package:angular2/src/core/annotations_impl/annotations.dart';
part 'part.dart';
@Component(selector: '[main]')
class MainComponent {
MainComponent();
}

View File

@ -0,0 +1,6 @@
part of main;
@Component(selector: '[part]')
class PartComponent {
PartComponent();
}