Index: lib/transformer.dart |
diff --git a/lib/transformer.dart b/lib/transformer.dart |
index 8133ba9bc00e114efcb4402bd1dac64a1446ca0e..390a2e270cc8d750ae1cc34af1473392d80f00ad 100644 |
--- a/lib/transformer.dart |
+++ b/lib/transformer.dart |
@@ -16,14 +16,34 @@ import 'package:html5lib/dom.dart' as dom; |
import 'package:html5lib/parser.dart' show parse; |
import 'package:path/path.dart' as path; |
-/// Removes the mirror-based initialization logic and replaces it with static |
-/// logic. |
+import 'build/initializer_plugin.dart'; |
+export 'build/initializer_plugin.dart'; |
+ |
+/// Create a new [Asset] which inlines your [Initializer] annotations into |
+/// a new file that bootstraps your application. |
+Asset generateBootstrapFile(Resolver resolver, Transform transform, |
+ AssetId primaryAssetId, AssetId newEntryPointId, |
+ {bool errorIfNotFound: true, List<InitializerPlugin> plugins, |
+ bool appendDefaultPlugin: true}) { |
+ if (appendDefaultPlugin) { |
+ if (plugins == null) plugins = []; |
+ plugins.add(const DefaultInitializerPlugin()); |
+ } |
+ return new _BootstrapFileBuilder( |
+ resolver, transform, primaryAssetId, newEntryPointId, errorIfNotFound, |
+ plugins: plugins).run(); |
+} |
+ |
+/// Transformer which removes the mirror-based initialization logic and replaces |
+/// it with static logic. |
class InitializeTransformer extends Transformer { |
final Resolvers _resolvers; |
final Iterable<Glob> _entryPointGlobs; |
final bool _errorIfNotFound; |
+ final List<InitializerPlugin> plugins; |
- InitializeTransformer(List<String> entryPoints, {bool errorIfNotFound: true}) |
+ InitializeTransformer(List<String> entryPoints, |
+ {bool errorIfNotFound: true, this.plugins}) |
: _entryPointGlobs = entryPoints.map((e) => new Glob(e)), |
_errorIfNotFound = errorIfNotFound, |
_resolvers = new Resolvers.fromMock({ |
@@ -107,24 +127,50 @@ class InitializeTransformer extends Transformer { |
} |
return _resolvers.get(transform, [primaryId]).then((resolver) { |
- new _BootstrapFileBuilder(resolver, transform, primaryId, |
- newEntryPointId, _errorIfNotFound).run(); |
+ transform.addOutput(generateBootstrapFile( |
+ resolver, transform, primaryId, newEntryPointId, |
+ errorIfNotFound: _errorIfNotFound, plugins: plugins)); |
resolver.release(); |
return newEntryPointId; |
}); |
}); |
} |
+ // Finds the first (and only) dart script on an html page and returns the |
+ // [AssetId] that points to it |
+ AssetId _findMainScript( |
+ dom.Document document, AssetId entryPoint, Transform transform) { |
+ var scripts = document.querySelectorAll('script[type="application/dart"]'); |
+ if (scripts.length != 1) { |
+ transform.logger.error('Expected exactly one dart script in $entryPoint ' |
+ 'but found ${scripts.length}.'); |
+ return null; |
+ } |
+ |
+ var src = scripts[0].attributes['src']; |
+ if (src == null) { |
+ // TODO(jakemac): Support inline scripts, |
+ transform.logger.error('Inline scripts are not supported at this time, ' |
+ 'see https://github.com/dart-lang/initialize/issues/20.'); |
+ return null; |
+ } |
+ |
+ return uriToAssetId( |
+ entryPoint, src, transform.logger, scripts[0].sourceSpan); |
+ } |
// Replaces script tags pointing to [originalDartFile] with [newDartFile] in |
// [entryPoint]. |
void _replaceEntryWithBootstrap(Transform transform, dom.Document document, |
AssetId entryPoint, AssetId originalDartFile, AssetId newDartFile) { |
var found = false; |
+ |
var scripts = document |
.querySelectorAll('script[type="application/dart"]') |
- .where((script) => uriToAssetId(entryPoint, script.attributes['src'], |
- transform.logger, script.sourceSpan) == originalDartFile) |
- .toList(); |
+ .where((script) { |
+ var assetId = uriToAssetId(entryPoint, script.attributes['src'], |
+ transform.logger, script.sourceSpan); |
+ return assetId == originalDartFile; |
+ }).toList(); |
if (scripts.length != 1) { |
transform.logger.error( |
@@ -136,29 +182,9 @@ class InitializeTransformer extends Transformer { |
from: path.dirname(entryPoint.path)); |
transform.addOutput(new Asset.fromString(entryPoint, document.outerHtml)); |
} |
- |
- AssetId _findMainScript( |
- dom.Document document, AssetId entryPoint, Transform transform) { |
- var scripts = document.querySelectorAll('script[type="application/dart"]'); |
- if (scripts.length != 1) { |
- transform.logger.error('Expected exactly one dart script in $entryPoint ' |
- 'but found ${scripts.length}.'); |
- return null; |
- } |
- |
- var src = scripts[0].attributes['src']; |
- // TODO(jakemac): Support inline scripts, |
- // https://github.com/dart-lang/initialize/issues/20 |
- if (src == null) { |
- transform.logger.error('Inline scripts are not supported at this time.'); |
- return null; |
- } |
- |
- return uriToAssetId( |
- entryPoint, src, transform.logger, scripts[0].sourceSpan); |
- } |
} |
+// Class which builds a bootstrap file. |
class _BootstrapFileBuilder { |
final Resolver _resolver; |
final Transform _transform; |
@@ -172,14 +198,19 @@ class _BootstrapFileBuilder { |
ClassElement _initializer; |
/// Queue for intialization annotations. |
- final _initQueue = new Queue<_InitializerData>(); |
+ final _initQueue = new Queue<InitializerData>(); |
/// All the annotations we have seen for each element |
final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>(); |
+ /// The list of [InitializerPlugin]s to apply. The first plugin which asks to |
+ /// be applied to a given initializer is the only one that will apply. |
+ List<InitializerPlugin> _plugins; |
+ |
TransformLogger _logger; |
_BootstrapFileBuilder(this._resolver, this._transform, this._entryPoint, |
- this._newEntryPoint, this._errorIfNotFound) { |
+ this._newEntryPoint, this._errorIfNotFound, |
+ {List<InitializerPlugin> plugins}) { |
_logger = _transform.logger; |
_initializeLibrary = |
_resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart')); |
@@ -190,15 +221,15 @@ class _BootstrapFileBuilder { |
'This file must be imported via $_entryPoint or a transitive ' |
'dependency.'); |
} |
+ _plugins = plugins != null ? plugins : [const DefaultInitializerPlugin()]; |
} |
- /// Adds the new entry point file to the transform. Should only be ran once. |
- void run() { |
+ /// Creates and returns the new bootstrap file. |
+ Asset run() { |
var entryLib = _resolver.getLibrary(_entryPoint); |
_readLibraries(entryLib); |
- _transform.addOutput( |
- new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib))); |
+ return new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib)); |
} |
/// Reads Initializer annotations on this library and all its dependencies in |
@@ -253,7 +284,7 @@ class _BootstrapFileBuilder { |
return !_seenAnnotations[element].contains(meta); |
}).forEach((ElementAnnotation meta) { |
_seenAnnotations[element].add(meta); |
- _initQueue.addLast(new _InitializerData(element, meta)); |
+ _initQueue.addLast(new InitializerData._(element, meta)); |
found = true; |
}); |
return found; |
@@ -270,18 +301,28 @@ class _BootstrapFileBuilder { |
importsBuffer.writeln("import 'package:initialize/initialize.dart';"); |
libraryPrefixes[entryLib] = 'i0'; |
- initializersBuffer.writeln(' initializers.addAll(['); |
+ initializersBuffer.writeln('initializers.addAll(['); |
while (_initQueue.isNotEmpty) { |
var next = _initQueue.removeFirst(); |
libraryPrefixes.putIfAbsent( |
- next.element.library, () => 'i${libraryPrefixes.length}'); |
- libraryPrefixes.putIfAbsent( |
- next.annotation.element.library, () => 'i${libraryPrefixes.length}'); |
+ next.targetElement.library, () => 'i${libraryPrefixes.length}'); |
+ libraryPrefixes.putIfAbsent(next.annotationElement.element.library, |
+ () => 'i${libraryPrefixes.length}'); |
+ |
+ // Run the first plugin which asks to be ran and then stop. |
+ var data = new InitializerPluginData( |
+ next, _newEntryPoint, libraryPrefixes, _resolver, _logger); |
+ var plugin = _plugins.firstWhere((p) => p.shouldApply(data), orElse: () { |
+ _logger.error('No InitializerPlugin handled the annotation: ' |
+ '${next.annotationElement} on: ${next.targetElement}.'); |
+ }); |
+ if (plugin == null) continue; |
- _writeInitializer(next, libraryPrefixes, initializersBuffer); |
+ var text = plugin.apply(data); |
+ if (text != null) initializersBuffer.writeln('$text,'); |
} |
- initializersBuffer.writeln(' ]);'); |
+ initializersBuffer.writeln(']);'); |
libraryPrefixes |
.forEach((lib, prefix) => _writeImport(lib, prefix, importsBuffer)); |
@@ -315,164 +356,6 @@ $initializersBuffer |
buffer.writeln(' as $prefix;'); |
} |
- _writeInitializer(_InitializerData data, |
- Map<LibraryElement, String> libraryPrefixes, StringBuffer buffer) { |
- final annotationElement = data.annotation.element; |
- final element = data.element; |
- |
- final metaPrefix = libraryPrefixes[annotationElement.library]; |
- var elementString; |
- if (element is LibraryElement) { |
- var segments = element.source.uri.pathSegments; |
- var package = segments[0]; |
- var libraryPath; |
- var packageString; |
- if (_newEntryPoint.package == package && |
- _newEntryPoint.path.startsWith('${segments[1]}/')) { |
- // reset `package` to null, we will do a relative path in this case. |
- packageString = 'null'; |
- libraryPath = path.url.relative( |
- path.url.joinAll(segments.getRange(1, segments.length)), |
- from: path.url.dirname(path.url.join(_newEntryPoint.path))); |
- } else if (segments[1] == 'lib') { |
- packageString = "'$package'"; |
- libraryPath = path.url.joinAll(segments.getRange(2, segments.length)); |
- } else { |
- _logger.error('Unable to import `${element.source.uri.path}` from ' |
- '${_newEntryPoint.path}.'); |
- } |
- |
- elementString = "const LibraryIdentifier(" |
- "#${element.name}, $packageString, '$libraryPath')"; |
- } else if (element is ClassElement || element is FunctionElement) { |
- elementString = |
- '${libraryPrefixes[data.element.library]}.${element.name}'; |
- } else { |
- _logger.error('Initializers can only be applied to top level functins, ' |
- 'libraries, and classes.'); |
- } |
- |
- if (annotationElement is ConstructorElement) { |
- var node = data.element.node; |
- List<Annotation> astMeta; |
- if (node is SimpleIdentifier) { |
- astMeta = node.parent.parent.metadata; |
- } else if (node is ClassDeclaration || node is FunctionDeclaration) { |
- astMeta = node.metadata; |
- } else { |
- _logger.error( |
- 'Initializer annotations are only supported on libraries, classes, ' |
- 'and top level methods. Found $node.'); |
- } |
- final annotation = |
- astMeta.firstWhere((m) => m.elementAnnotation == data.annotation); |
- final clazz = annotation.name; |
- final constructor = annotation.constructorName == null |
- ? '' |
- : '.${annotation.constructorName}'; |
- // TODO(jakemac): Support more than raw values here |
- // https://github.com/dart-lang/static_init/issues/5 |
- final args = _buildArgsString(annotation.arguments, libraryPrefixes); |
- buffer.write(''' |
- new InitEntry(const $metaPrefix.${clazz}$constructor$args, $elementString), |
-'''); |
- } else if (annotationElement is PropertyAccessorElement) { |
- buffer.write(''' |
- new InitEntry($metaPrefix.${annotationElement.name}, $elementString), |
-'''); |
- } else { |
- _logger.error('Unsupported annotation type. Only constructors and ' |
- 'properties are supported as initializers.'); |
- } |
- } |
- |
- String _buildArgsString( |
- ArgumentList args, Map<LibraryElement, String> libraryPrefixes) { |
- var buffer = new StringBuffer(); |
- buffer.write('('); |
- var first = true; |
- for (var arg in args.arguments) { |
- if (!first) buffer.write(', '); |
- first = false; |
- |
- Expression expression; |
- if (arg is NamedExpression) { |
- buffer.write('${arg.name.label.name}: '); |
- expression = arg.expression; |
- } else { |
- expression = arg; |
- } |
- |
- buffer.write(_expressionString(expression, libraryPrefixes)); |
- } |
- buffer.write(')'); |
- return buffer.toString(); |
- } |
- |
- String _expressionString( |
- Expression expression, Map<LibraryElement, String> libraryPrefixes) { |
- var buffer = new StringBuffer(); |
- if (expression is StringLiteral) { |
- var value = expression.stringValue; |
- if (value == null) { |
- _logger.error('Only const strings are allowed in initializer ' |
- 'expressions, found $expression'); |
- } |
- value = value.replaceAll(r'\', r'\\').replaceAll(r"'", r"\'"); |
- buffer.write("'$value'"); |
- } else if (expression is BooleanLiteral || |
- expression is DoubleLiteral || |
- expression is IntegerLiteral || |
- expression is NullLiteral) { |
- buffer.write('${expression}'); |
- } else if (expression is ListLiteral) { |
- buffer.write('const ['); |
- var first = true; |
- for (Expression listExpression in expression.elements) { |
- if (!first) buffer.write(', '); |
- first = false; |
- buffer.write(_expressionString(listExpression, libraryPrefixes)); |
- } |
- buffer.write(']'); |
- } else if (expression is MapLiteral) { |
- buffer.write('const {'); |
- var first = true; |
- for (MapLiteralEntry entry in expression.entries) { |
- if (!first) buffer.write(', '); |
- first = false; |
- buffer.write(_expressionString(entry.key, libraryPrefixes)); |
- buffer.write(': '); |
- buffer.write(_expressionString(entry.value, libraryPrefixes)); |
- } |
- buffer.write('}'); |
- } else if (expression is Identifier) { |
- var element = expression.bestElement; |
- if (element == null || !element.isPublic) { |
- _logger.error('Private constants are not supported in intializer ' |
- 'constructors, found $element.'); |
- } |
- libraryPrefixes.putIfAbsent( |
- element.library, () => 'i${libraryPrefixes.length}'); |
- |
- buffer.write('${libraryPrefixes[element.library]}.'); |
- if (element is ClassElement) { |
- buffer.write(element.name); |
- } else if (element is PropertyAccessorElement) { |
- var variable = element.variable; |
- if (variable is FieldElement) { |
- buffer.write('${variable.enclosingElement.name}.'); |
- } |
- buffer.write('${variable.name}'); |
- } else { |
- _logger.error('Unsupported argument to initializer constructor.'); |
- } |
- } else { |
- _logger.error('Only literals and identifiers are allowed for initializer ' |
- 'expressions, found $expression.'); |
- } |
- return buffer.toString(); |
- } |
- |
bool _isInitializer(InterfaceType type) { |
// If `_initializer` wasn't found then it was never loaded (even |
// transitively), and so no annotations can be initializers. |
@@ -562,12 +445,43 @@ $initializersBuffer |
})).map((import) => import.importedLibrary); |
} |
-// Element/ElementAnnotation pair. |
-class _InitializerData { |
- final Element element; |
- final ElementAnnotation annotation; |
+/// An [Initializer] annotation and the target of that annotation. |
+class InitializerData { |
+ /// The target [Element] of the annotation. |
+ final Element targetElement; |
+ |
+ /// The [ElementAnnotation] representing the annotation itself. |
+ final ElementAnnotation annotationElement; |
+ |
+ AstNode _targetNode; |
+ |
+ /// The target [AstNode] of the annotation. |
+ // TODO(jakemac): We at least cache this for now, but ideally `targetElement` |
+ // would actually be the getter, and `targetNode` would be the property. |
+ AstNode get targetNode { |
+ if (_targetNode == null) _targetNode = targetElement.node; |
+ return _targetNode; |
+ } |
+ |
+ /// The [Annotation] representing the annotation itself. |
+ Annotation get annotationNode { |
+ var annotatedNode; |
+ if (targetNode is SimpleIdentifier && |
+ targetNode.parent is LibraryIdentifier) { |
+ annotatedNode = targetNode.parent.parent; |
+ } else if (targetNode is ClassDeclaration || |
+ targetNode is FunctionDeclaration) { |
+ annotatedNode = targetNode; |
+ } else { |
+ return null; |
+ } |
+ if (annotatedNode is! AnnotatedNode) return null; |
+ var astMeta = annotatedNode.metadata; |
+ |
+ return astMeta.firstWhere((m) => m.elementAnnotation == annotationElement); |
+ } |
- _InitializerData(this.element, this.annotation); |
+ InitializerData._(this.targetElement, this.annotationElement); |
} |
// Reads a file list from a barback settings configuration field. |