feat(transformers): directive aliases in Dart transformers (fix #1747)

This commit is contained in:
Sigmund Cherem
2015-07-17 13:21:37 -07:00
parent 46502e4d61
commit fd46b49ea6
43 changed files with 910 additions and 287 deletions

View File

@ -1,3 +1,4 @@
import {Injectable} from 'angular2/di';
import {StringMapWrapper, ListWrapper, List} from 'angular2/src/facade/collection';
import {isPresent, isArray} from 'angular2/src/facade/lang';
import * as modelModule from './model';
@ -67,6 +68,7 @@ import * as modelModule from './model';
*
* ```
*/
@Injectable()
export class FormBuilder {
group(controlsConfig: StringMap<string, any>,
extra: StringMap<string, any> = null): modelModule.ControlGroup {

View File

@ -6,6 +6,9 @@ const REFLECTOR_VAR_NAME = 'reflector';
const TRANSFORM_DYNAMIC_MODE = 'transform_dynamic';
const DEPS_EXTENSION = '.ng_deps.dart';
const META_EXTENSION = '.ng_meta.json';
// TODO(sigmund): consider merging into .ng_meta by generating local metadata
// upfront (rather than extracting it from ng_deps).
const ALIAS_EXTENSION = '.aliases.json';
const REFLECTION_CAPABILITIES_NAME = 'ReflectionCapabilities';
const REGISTER_TYPE_METHOD_NAME = 'registerType';
const REGISTER_GETTERS_METHOD_NAME = 'registerGetters';
@ -20,6 +23,10 @@ String toMetaExtension(String uri) =>
String toDepsExtension(String uri) =>
_toExtension(uri, const [META_EXTENSION, '.dart'], DEPS_EXTENSION);
/// Returns `uri` with its extension updated to [ALIAS_EXTENSION].
String toAliasExtension(String uri) =>
_toExtension(uri, const [DEPS_EXTENSION, '.dart'], ALIAS_EXTENSION);
/// Returns `uri` with its extension updated to `toExtension` if its
/// extension is currently in `fromExtension`.
String _toExtension(

View File

@ -0,0 +1,88 @@
library angular2.transform.common.ng_meta;
import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/render/dom/convert.dart';
import 'logging.dart';
/// Metadata about directives and directive aliases.
///
/// [NgMeta] is used in three stages of the transformation process. First we
/// store directive aliases exported from a single file in an [NgMeta] instance.
/// Later we use another [NgMeta] instance to store more information about a
/// single file, including both directive aliases and directives extracted from
/// the corresponding `.ng_deps.dart` file. Further down the compilation
/// process, the template compiler needs to reason about the namespace of import
/// prefixes, so it will combine multple [NgMeta] instances together if they
/// were imported into a file with the same prefix.
///
/// Instances of this class are serialized into `.aliases.json` and
/// `.ng_meta.json` files as intermediate assets to make the compilation process
/// easier.
class NgMeta {
/// Directive metadata for each type annotated as a directive.
final Map<String, DirectiveMetadata> types;
/// List of other types and names associated with a given name.
final Map<String, List<String>> aliases;
NgMeta(this.types, this.aliases);
NgMeta.empty() : this({}, {});
bool get isEmpty => types.isEmpty && aliases.isEmpty;
/// Parse from the serialized form produced by [toJson].
factory NgMeta.fromJson(Map json) {
var types = {};
var aliases = {};
for (var key in json.keys) {
var entry = json[key];
if (entry['kind'] == 'type') {
types[key] = directiveMetadataFromMap(entry['value']);
} else if (entry['kind'] == 'alias') {
aliases[key] = entry['value'];
}
}
return new NgMeta(types, aliases);
}
/// Serialized representation of this instance.
Map toJson() {
var result = {};
types.forEach((k, v) {
result[k] = {'kind': 'type', 'value': directiveMetadataToMap(v)};
});
aliases.forEach((k, v) {
result[k] = {'kind': 'alias', 'value': v};
});
return result;
}
/// Merge into this instance all information from [other].
void addAll(NgMeta other) {
types.addAll(other.types);
aliases.addAll(other.aliases);
}
/// Returns the metadata for every type associated with the given [alias].
List<DirectiveMetadata> flatten(String alias) {
var result = [];
var seen = new Set();
helper(name) {
if (!seen.add(name)) {
logger.warning('Circular alias dependency for "$name".');
return;
}
if (types.containsKey(name)) {
result.add(types[name]);
} else if (aliases.containsKey(name)) {
aliases[name].forEach(helper);
} else {
logger.warning('Unknown alias: "$name".');
}
}
helper(alias);
return result;
}
}

View File

@ -1,38 +1,49 @@
library angular2.transform.directive_metadata_extractor.extractor;
import 'dart:async';
import 'dart:convert';
import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/render/api.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/ng_deps.dart';
import 'package:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart';
import 'package:code_transformers/assets.dart';
/// Returns a map from a class name (that is, its `Identifier` stringified)
/// to [DirectiveMetadata] for all `Directive`-annotated classes visible
/// in a file importing `entryPoint`. That is, this includes all
/// `Directive` annotated classes in `entryPoint` and any assets which it
/// `export`s.
/// Returns `null` if there are no `Directive`-annotated classes in
/// `entryPoint`.
Future<Map<String, DirectiveMetadata>> extractDirectiveMetadata(
AssetReader reader, AssetId entryPoint) async {
/// Returns [NgMeta] associated with [entryPoint].
///
/// This includes entries for every `Directive`-annotated classes and
/// constants that match the directive-aliases pattern, which are visible in a
/// file importing `entryPoint`. That is, this includes all `Directive`
/// annotated public classes in `entryPoint`, all `DirectiveAlias` annotated
/// public variables, and any assets which it `export`s. Returns an empty
/// [NgMeta] if there are no `Directive`-annotated classes in `entryPoint`.
Future<NgMeta> extractDirectiveMetadata(
AssetReader reader, AssetId entryPoint) {
return _extractDirectiveMetadataRecursive(reader, entryPoint);
}
var _nullFuture = new Future.value(null);
Future<Map<String, DirectiveMetadata>> _extractDirectiveMetadataRecursive(
Future<NgMeta> _extractDirectiveMetadataRecursive(
AssetReader reader, AssetId entryPoint) async {
if (!(await reader.hasInput(entryPoint))) return null;
var ngMeta = new NgMeta.empty();
if (!(await reader.hasInput(entryPoint))) return ngMeta;
var ngDeps = await NgDeps.parse(reader, entryPoint);
var baseMap = _metadataMapFromNgDeps(ngDeps);
_extractMetadata(ngDeps, ngMeta);
return Future.wait(ngDeps.exports.map((export) {
var aliasesFile =
new AssetId(entryPoint.package, toAliasExtension(entryPoint.path));
if (await reader.hasInput(aliasesFile)) {
ngMeta.addAll(new NgMeta.fromJson(
JSON.decode(await reader.readAsString(aliasesFile))));
}
await Future.wait(ngDeps.exports.map((export) {
var uri = stringLiteralToString(export.uri);
if (uri.startsWith('dart:')) return _nullFuture;
@ -41,25 +52,16 @@ Future<Map<String, DirectiveMetadata>> _extractDirectiveMetadataRecursive(
errorOnAbsolute: false);
if (assetId == entryPoint) return _nullFuture;
return _extractDirectiveMetadataRecursive(reader, assetId)
.then((exportMap) {
if (exportMap != null) {
if (baseMap == null) {
baseMap = exportMap;
} else {
baseMap.addAll(exportMap);
}
}
});
})).then((_) => baseMap);
.then(ngMeta.addAll);
}));
return ngMeta;
}
Map<String, DirectiveMetadata> _metadataMapFromNgDeps(NgDeps ngDeps) {
if (ngDeps == null || ngDeps.registeredTypes.isEmpty) return null;
var retVal = <String, DirectiveMetadata>{};
ngDeps.registeredTypes.forEach((rType) {
if (rType.directiveMetadata != null) {
retVal['${rType.typeName}'] = rType.directiveMetadata;
}
// TODO(sigmund): rather than having to parse it from generated code. we should
// be able to produce directly all information we need for ngMeta.
void _extractMetadata(NgDeps ngDeps, NgMeta ngMeta) {
if (ngDeps == null) return;
ngDeps.registeredTypes.forEach((type) {
ngMeta.types[type.typeName.name] = type.directiveMetadata;
});
return retVal;
}

View File

@ -3,7 +3,6 @@ library angular2.transform.directive_metadata_extractor.transformer;
import 'dart:async';
import 'dart:convert';
import 'package:angular2/src/render/dom/convert.dart';
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';
@ -29,14 +28,10 @@ class DirectiveMetadataExtractor extends Transformer {
var reader = new AssetReader.fromTransform(transform);
var fromAssetId = transform.primaryInput.id;
var metadataMap = await extractDirectiveMetadata(reader, fromAssetId);
if (metadataMap != null) {
var jsonMap = <String, Map>{};
metadataMap.forEach((k, v) {
jsonMap[k] = directiveMetadataToMap(v);
});
var ngMeta = await extractDirectiveMetadata(reader, fromAssetId);
if (ngMeta != null && !ngMeta.isEmpty) {
transform.addOutput(new Asset.fromString(
_outputAssetId(fromAssetId), _encoder.convert(jsonMap)));
_outputAssetId(fromAssetId), _encoder.convert(ngMeta.toJson())));
}
}, errorMessage: 'Extracting ng metadata failed.');
}

View File

@ -11,6 +11,7 @@ import 'package:angular2/src/transform/common/interface_matcher.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:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart' show AssetId;
import 'package:path/path.dart' as path;
@ -23,14 +24,19 @@ 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.
Future<String> createNgDeps(
AssetReader reader, AssetId assetId, AnnotationMatcher annotationMatcher,
Future<String> createNgDeps(AssetReader reader, AssetId assetId,
AnnotationMatcher annotationMatcher, NgMeta ngMeta,
{bool inlineViews}) async {
// TODO(kegluneq): Shortcut if we can determine that there are no
// [Directive]s present, taking into account `export`s.
var writer = new AsyncStringWriter();
var visitor = new CreateNgDepsVisitor(writer, assetId,
new XhrImpl(reader, assetId), annotationMatcher, _interfaceMatcher,
var visitor = new CreateNgDepsVisitor(
writer,
assetId,
new XhrImpl(reader, assetId),
annotationMatcher,
_interfaceMatcher,
ngMeta,
inlineViews: inlineViews);
var code = await reader.readAsString(assetId);
parseCompilationUnit(code, name: assetId.path).accept(visitor);
@ -49,10 +55,19 @@ InterfaceMatcher _interfaceMatcher = new InterfaceMatcher();
/// associated .ng_deps.dart file.
class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
final AsyncStringWriter writer;
/// Output ngMeta information about aliases.
// TODO(sigmund): add more to ngMeta. Currently this only contains aliasing
// information, but we could produce here all the metadata we need and avoid
// parsing the ngdeps files later.
final NgMeta ngMeta;
/// Whether an Angular 2 `Injectable` has been found.
bool _foundNgInjectable = false;
/// Whether this library `imports` or `exports` any non-'dart:' libraries.
bool _usesNonLangLibs = false;
/// Whether we have written an import of base file
/// (the file we are processing).
bool _wroteBaseLibImport = false;
@ -66,8 +81,13 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
/// The assetId for the file which we are parsing.
final AssetId assetId;
CreateNgDepsVisitor(AsyncStringWriter writer, AssetId assetId, XHR xhr,
AnnotationMatcher annotationMatcher, InterfaceMatcher interfaceMatcher,
CreateNgDepsVisitor(
AsyncStringWriter writer,
AssetId assetId,
XHR xhr,
AnnotationMatcher annotationMatcher,
InterfaceMatcher interfaceMatcher,
this.ngMeta,
{bool inlineViews})
: writer = writer,
_copyVisitor = new ToSourceVisitor(writer),
@ -223,6 +243,29 @@ class CreateNgDepsVisitor extends Object with SimpleAstVisitor<Object> {
return null;
}
@override
Object visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
// We process any top-level declaration that fits the directive-alias
// declaration pattern. Ideally we would use an annotation on the field to
// help us filter out only what's needed, but unfortunately TypeScript
// doesn't support decorators on variable declarations (see
// angular/angular#1747 and angular/ts2dart#249 for context).
outer: for (var variable in node.variables.variables) {
var initializer = variable.initializer;
if (initializer != null && initializer is ListLiteral) {
var otherNames = [];
for (var exp in initializer.elements) {
// Only simple identifiers are supported for now.
// TODO(sigmund): add support for prefixes (see issue #3232).
if (exp is! SimpleIdentifier) continue outer;
otherNames.add(exp.name);
}
ngMeta.aliases[variable.name.name] = otherNames;
}
}
return null;
}
Object _nodeToSource(AstNode node) {
if (node == null) return null;
return node.accept(_copyVisitor);

View File

@ -1,11 +1,13 @@
library angular2.transform.directive_processor.transformer;
import 'dart:async';
import 'dart:convert';
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';
import 'package:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart';
import 'rewriter.dart';
@ -32,8 +34,9 @@ class DirectiveProcessor extends Transformer {
await log.initZoned(transform, () async {
var asset = transform.primaryInput;
var reader = new AssetReader.fromTransform(transform);
var ngMeta = new NgMeta.empty();
var ngDepsSrc = await createNgDeps(
reader, asset.id, options.annotationMatcher,
reader, asset.id, options.annotationMatcher, ngMeta,
inlineViews: options.inlineViews);
if (ngDepsSrc != null && ngDepsSrc.isNotEmpty) {
var ngDepsAssetId =
@ -44,6 +47,12 @@ class DirectiveProcessor extends Transformer {
}
transform.addOutput(new Asset.fromString(ngDepsAssetId, ngDepsSrc));
}
if (!ngMeta.isEmpty) {
var ngAliasesId =
transform.primaryInput.id.changeExtension(ALIAS_EXTENSION);
transform.addOutput(new Asset.fromString(ngAliasesId,
new JsonEncoder.withIndent(" ").convert(ngMeta.toJson())));
}
}, errorMessage: 'Processing ng directives failed.');
}
}

View File

@ -5,11 +5,11 @@ import 'dart:convert';
import 'package:analyzer/analyzer.dart';
import 'package:angular2/src/render/api.dart';
import 'package:angular2/src/render/dom/convert.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/ng_deps.dart';
import 'package:angular2/src/transform/common/ng_meta.dart';
import 'package:barback/barback.dart';
import 'package:code_transformers/assets.dart';
@ -60,18 +60,20 @@ class _ViewDefinitionCreator {
var ngDeps = await ngDepsFuture;
var retVal = <RegisteredType, ViewDefinitionEntry>{};
var visitor = new _TemplateExtractVisitor(await _createMetadataMap());
var visitor = new _TemplateExtractVisitor(await _extractNgMeta());
ngDeps.registeredTypes.forEach((rType) {
visitor.reset();
rType.annotations.accept(visitor);
if (visitor.viewDef != null) {
// Note: we use '' because the current file maps to the default prefix.
var ngMeta = visitor._metadataMap[''];
var typeName = '${rType.typeName}';
var hostMetadata = null;
if (visitor._metadataMap.containsKey(typeName)) {
hostMetadata = visitor._metadataMap[typeName];
if (ngMeta.types.containsKey(typeName)) {
hostMetadata = ngMeta.types[typeName];
visitor.viewDef.componentId = hostMetadata.id;
} else {
logger.error('Missing component "$typeName" in metadata map',
logger.warning('Missing component "$typeName" in metadata map',
asset: entryPoint);
visitor.viewDef.componentId = _getComponentId(entryPoint, typeName);
}
@ -91,7 +93,7 @@ class _ViewDefinitionCreator {
var ngDeps = await ngDepsFuture;
var importAssetToPrefix = <AssetId, String>{};
// Include the `.ng_meta.dart` file associated with `entryPoint`.
// Include the `.ng_meta.json` file associated with `entryPoint`.
importAssetToPrefix[new AssetId(
entryPoint.package, toMetaExtension(entryPoint.path))] = null;
@ -137,24 +139,24 @@ class _ViewDefinitionCreator {
/// ...<any other entries>...
/// }
/// ```
Future<Map<String, DirectiveMetadata>> _createMetadataMap() async {
Future<Map<String, NgMeta>> _extractNgMeta() async {
var importAssetToPrefix = await _createImportAssetToPrefixMap();
var retVal = <String, DirectiveMetadata>{};
var retVal = <String, NgMeta>{};
for (var importAssetId in importAssetToPrefix.keys) {
var prefix = importAssetToPrefix[importAssetId];
if (prefix == null) prefix = '';
var ngMeta = retVal.putIfAbsent(prefix, () => new NgMeta.empty());
var metaAssetId = new AssetId(
importAssetId.package, toMetaExtension(importAssetId.path));
if (await reader.hasInput(metaAssetId)) {
try {
var json = await reader.readAsString(metaAssetId);
var jsonMap = JSON.decode(json);
jsonMap.forEach((className, metaDataMap) {
var prefixStr = importAssetToPrefix[importAssetId];
var key = prefixStr != null ? '$prefixStr.$className' : className;
var value = directiveMetadataFromMap(metaDataMap)
..id = _getComponentId(importAssetId, className);
retVal[key] = value;
var json = JSON.decode(await reader.readAsString(metaAssetId));
var newMetadata = new NgMeta.fromJson(json);
newMetadata.types.forEach((className, metadata) {
metadata.id = _getComponentId(importAssetId, className);
});
ngMeta.addAll(newMetadata);
} catch (ex, stackTrace) {
logger.warning('Failed to decode: $ex, $stackTrace',
asset: metaAssetId);
@ -169,7 +171,7 @@ class _ViewDefinitionCreator {
/// [RegisterType] object and pulling out [ViewDefinition] information.
class _TemplateExtractVisitor extends Object with RecursiveAstVisitor<Object> {
ViewDefinition viewDef = null;
final Map<String, DirectiveMetadata> _metadataMap;
final Map<String, NgMeta> _metadataMap;
final ConstantEvaluator _evaluator = new ConstantEvaluator();
_TemplateExtractVisitor(this._metadataMap);
@ -237,28 +239,35 @@ class _TemplateExtractVisitor extends Object with RecursiveAstVisitor<Object> {
if (viewDef == null) return;
if (node is! ListLiteral) {
logger.error(
'Angular 2 currently only supports list literals as values for'
' "directives". Source: $node');
logger.error('Angular 2 currently only supports list literals as values '
'for "directives". Source: $node');
return;
}
var directiveList = (node as ListLiteral);
for (var node in directiveList.elements) {
if (node is! SimpleIdentifier && node is! PrefixedIdentifier) {
var ngMeta;
var name;
if (node is SimpleIdentifier) {
ngMeta = _metadataMap[''];
name = node.name;
} else if (node is PrefixedIdentifier) {
ngMeta = _metadataMap[node.prefix.name];
name = node.name;
} else {
logger.error(
'Angular 2 currently only supports simple and prefixed identifiers '
'as values for "directives". Source: $node');
return;
}
var name = '$node';
if (_metadataMap.containsKey(name)) {
viewDef.directives.add(_metadataMap[name]);
if (ngMeta.types.containsKey(name)) {
viewDef.directives.add(ngMeta.types[name]);
} else if (ngMeta.aliases.containsKey(name)) {
viewDef.directives.addAll(ngMeta.flatten(name));
} else {
logger.warning('Could not find Directive entry for $name. '
'Please be aware that reusable, pre-defined lists of Directives '
'(aka "directive aliases") are not yet supported and will cause '
'your application to misbehave. '
'See https://github.com/angular/angular/issues/1747 for details.');
logger.warning('Could not find Directive entry for $node. '
'Please be aware that Dart transformers have limited support for '
'reusable, pre-defined lists of Directives (aka '
'"directive aliases"). See https://goo.gl/d8XPt0 for details.');
}
}
}