| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /** Transfomer that combines multiple dart script tags into a single one. */ |
| library polymer.src.build.script_compactor; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| |
| import 'package:analyzer/src/generated/ast.dart'; |
| import 'package:analyzer/src/generated/error.dart'; |
| import 'package:analyzer/src/generated/parser.dart'; |
| import 'package:analyzer/src/generated/scanner.dart'; |
| import 'package:barback/barback.dart'; |
| import 'package:html5lib/parser.dart' show parseFragment; |
| import 'package:path/path.dart' as path; |
| import 'package:source_maps/span.dart' show SourceFile; |
| |
| import 'code_extractor.dart'; // import just for documentation. |
| import 'common.dart'; |
| |
| /** |
| * Combines Dart script tags into a single script tag, and creates a new Dart |
| * file that calls the main function of each of the original script tags. |
| * |
| * This transformer assumes that all script tags point to external files. To |
| * support script tags with inlined code, use this transformer after running |
| * [InlineCodeExtractor] on an earlier phase. |
| * |
| * Internally, this transformer will convert each script tag into an import |
| * statement to a library, and then uses `initPolymer` (see polymer.dart) to |
| * process `@initMethod` and `@CustomTag` annotations in those libraries. |
| */ |
| class ScriptCompactor extends Transformer with PolymerTransformer { |
| final TransformOptions options; |
| |
| ScriptCompactor(this.options); |
| |
| /** Only run on entry point .html files. */ |
| Future<bool> isPrimary(Asset input) => |
| new Future.value(options.isHtmlEntryPoint(input.id)); |
| |
| Future apply(Transform transform) { |
| var id = transform.primaryInput.id; |
| var secondaryId = id.addExtension('.scriptUrls'); |
| var logger = transform.logger; |
| return readPrimaryAsHtml(transform).then((document) { |
| return transform.readInputAsString(secondaryId).then((libraryIds) { |
| var libraries = (JSON.decode(libraryIds) as Iterable).map( |
| (data) => new AssetId.deserialize(data)).toList(); |
| var mainLibraryId; |
| var mainScriptTag; |
| bool changed = false; |
| |
| for (var tag in document.queryAll('script')) { |
| var src = tag.attributes['src']; |
| if (src == 'packages/polymer/boot.js') { |
| tag.remove(); |
| continue; |
| } |
| if (tag.attributes['type'] != 'application/dart') continue; |
| if (src == null) { |
| logger.warning('unexpected script without a src url. The ' |
| 'ScriptCompactor transformer should run after running the ' |
| 'InlineCodeExtractor', span: tag.sourceSpan); |
| continue; |
| } |
| if (mainLibraryId != null) { |
| logger.warning('unexpected script. Only one Dart script tag ' |
| 'per document is allowed.', span: tag.sourceSpan); |
| tag.remove(); |
| continue; |
| } |
| mainLibraryId = resolve(id, src, logger, tag.sourceSpan); |
| mainScriptTag = tag; |
| } |
| |
| if (mainScriptTag == null) { |
| // We didn't find any main library, nothing to do. |
| transform.addOutput(transform.primaryInput); |
| return null; |
| } |
| |
| // Emit the bootstrap .dart file |
| var bootstrapId = id.addExtension('_bootstrap.dart'); |
| mainScriptTag.attributes['src'] = |
| path.url.basename(bootstrapId.path); |
| |
| libraries.add(mainLibraryId); |
| var urls = libraries.map((id) => assetUrlFor(id, bootstrapId, logger)) |
| .where((url) => url != null).toList(); |
| var buffer = new StringBuffer()..writeln(MAIN_HEADER); |
| int i = 0; |
| for (; i < urls.length; i++) { |
| buffer.writeln("import '${urls[i]}' as i$i;"); |
| } |
| |
| buffer..write('\n') |
| ..writeln('void main() {') |
| ..writeln(' configureForDeployment(['); |
| |
| // Inject @CustomTag and @initMethod initializations for each library |
| // that is sourced in a script tag. |
| i = 0; |
| return Future.forEach(libraries, (lib) { |
| return _initializersOf(lib, transform, logger).then((initializers) { |
| for (var init in initializers) { |
| var code = init.asCode('i$i'); |
| buffer.write(" $code,\n"); |
| } |
| i++; |
| }); |
| }).then((_) { |
| buffer..writeln(' ]);') |
| ..writeln(' i${urls.length - 1}.main();') |
| ..writeln('}'); |
| |
| transform.addOutput(new Asset.fromString( |
| bootstrapId, buffer.toString())); |
| transform.addOutput(new Asset.fromString(id, document.outerHtml)); |
| }); |
| }); |
| }); |
| } |
| |
| /** |
| * Computes the initializers of [dartLibrary]. That is, a closure that calls |
| * Polymer.register for each @CustomTag, and any public top-level methods |
| * labeled with @initMethod. |
| */ |
| Future<List<_Initializer>> _initializersOf( |
| AssetId dartLibrary, Transform transform, TransformLogger logger) { |
| var initializers = []; |
| return transform.readInputAsString(dartLibrary).then((code) { |
| var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); |
| var unit = _parseCompilationUnit(code); |
| |
| return Future.forEach(unit.directives, (directive) { |
| // Include anything from parts. |
| if (directive is PartDirective) { |
| var targetId = resolve(dartLibrary, directive.uri.stringValue, |
| logger, _getSpan(file, directive)); |
| return _initializersOf(targetId, transform, logger) |
| .then(initializers.addAll); |
| } |
| |
| // Similarly, include anything from exports except what's filtered by |
| // the show/hide combinators. |
| if (directive is ExportDirective) { |
| var targetId = resolve(dartLibrary, directive.uri.stringValue, |
| logger, _getSpan(file, directive)); |
| return _initializersOf(targetId, transform, logger) |
| .then((r) => _processExportDirective(directive, r, initializers)); |
| } |
| }).then((_) { |
| // Scan the code for classes and top-level functions. |
| for (var node in unit.declarations) { |
| if (node is ClassDeclaration) { |
| _processClassDeclaration(node, initializers, file, logger); |
| } else if (node is FunctionDeclaration && |
| node.metadata.any(_isInitMethodAnnotation)) { |
| _processFunctionDeclaration(node, initializers, file, logger); |
| } |
| } |
| return initializers; |
| }); |
| }); |
| } |
| |
| static String _simpleUriForSource(AssetId source) => |
| source.path.startsWith('lib/') |
| ? 'package:${source.package}/${source.path.substring(4)}' : source.path; |
| |
| /** |
| * Filter [exportedInitializers] according to [directive]'s show/hide |
| * combinators and add the result to [initializers]. |
| */ |
| // TODO(sigmund): call the analyzer's resolver instead? |
| static _processExportDirective(ExportDirective directive, |
| List<_Initializer> exportedInitializers, |
| List<_Initializer> initializers) { |
| for (var combinator in directive.combinators) { |
| if (combinator is ShowCombinator) { |
| var show = combinator.shownNames.map((n) => n.name).toSet(); |
| exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); |
| } else if (combinator is HideCombinator) { |
| var hide = combinator.hiddenNames.map((n) => n.name).toSet(); |
| exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); |
| } |
| } |
| initializers.addAll(exportedInitializers); |
| } |
| |
| /** |
| * Add an initializer to register [node] as a polymer element if it contains |
| * an appropriate [CustomTag] annotation. |
| */ |
| static _processClassDeclaration(ClassDeclaration node, |
| List<_Initializer> initializers, SourceFile file, |
| TransformLogger logger) { |
| for (var meta in node.metadata) { |
| if (!_isCustomTagAnnotation(meta)) continue; |
| var args = meta.arguments.arguments; |
| if (args == null || args.length == 0) { |
| logger.error('Missing argument in @CustomTag annotation', |
| span: _getSpan(file, meta)); |
| continue; |
| } |
| |
| var tagName = args[0].stringValue; |
| var typeName = node.name.name; |
| if (typeName.startsWith('_')) { |
| logger.error('@CustomTag is no longer supported on private ' |
| 'classes: $tagName', span: _getSpan(file, node.name)); |
| continue; |
| } |
| initializers.add(new _CustomTagInitializer(tagName, typeName)); |
| } |
| } |
| |
| /** a method initializer for [function]. */ |
| static _processFunctionDeclaration(FunctionDeclaration function, |
| List<_Initializer> initializers, SourceFile file, |
| TransformLogger logger) { |
| var name = function.name.name; |
| if (name.startsWith('_')) { |
| logger.error('@initMethod is no longer supported on private ' |
| 'functions: $name', span: _getSpan(file, function.name)); |
| return; |
| } |
| initializers.add(new _InitMethodInitializer(name)); |
| } |
| } |
| |
| /** Parse [code] using analyzer. */ |
| CompilationUnit _parseCompilationUnit(String code) { |
| var errorListener = new _ErrorCollector(); |
| var reader = new CharSequenceReader(code); |
| var scanner = new Scanner(null, reader, errorListener); |
| var token = scanner.tokenize(); |
| var parser = new Parser(null, errorListener); |
| return parser.parseCompilationUnit(token); |
| } |
| |
| class _ErrorCollector extends AnalysisErrorListener { |
| final errors = <AnalysisError>[]; |
| onError(error) => errors.add(error); |
| } |
| |
| // TODO(sigmund): consider support for importing annotations with prefixes. |
| bool _isInitMethodAnnotation(Annotation node) => |
| node.name.name == 'initMethod' && node.constructorName == null && |
| node.arguments == null; |
| bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; |
| |
| abstract class _Initializer { |
| String get symbolName; |
| String asCode(String prefix); |
| } |
| |
| class _InitMethodInitializer implements _Initializer { |
| String methodName; |
| String get symbolName => methodName; |
| _InitMethodInitializer(this.methodName); |
| |
| String asCode(String prefix) => "$prefix.$methodName"; |
| } |
| |
| class _CustomTagInitializer implements _Initializer { |
| String tagName; |
| String typeName; |
| String get symbolName => typeName; |
| _CustomTagInitializer(this.tagName, this.typeName); |
| |
| String asCode(String prefix) => |
| "() => Polymer.register('$tagName', $prefix.$typeName)"; |
| } |
| |
| _getSpan(SourceFile file, ASTNode node) => file.span(node.offset, node.end); |
| |
| const MAIN_HEADER = """ |
| library app_bootstrap; |
| |
| import 'package:polymer/polymer.dart'; |
| """; |