Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Transfomer that combines multiple dart script tags into a single one. | 5 /// Transfomer that combines multiple dart script tags into a single one. |
| 6 library polymer.src.build.script_compactor; | 6 library polymer.src.build.script_compactor; |
|
Siggi Cherem (dart-lang)
2014/03/06 22:45:23
there is no new logic on this file, just code refa
| |
| 7 | 7 |
| 8 import 'dart:async'; | 8 import 'dart:async'; |
| 9 import 'dart:convert'; | 9 import 'dart:convert'; |
| 10 | 10 |
| 11 import 'package:html5lib/dom.dart' show Document, Element; | |
| 11 import 'package:analyzer/src/generated/ast.dart'; | 12 import 'package:analyzer/src/generated/ast.dart'; |
| 12 import 'package:barback/barback.dart'; | 13 import 'package:barback/barback.dart'; |
| 13 import 'package:path/path.dart' as path; | 14 import 'package:path/path.dart' as path; |
| 14 import 'package:source_maps/span.dart' show SourceFile; | 15 import 'package:source_maps/span.dart' show SourceFile; |
| 15 | 16 |
| 16 import 'import_inliner.dart' show ImportInliner; // just for docs. | 17 import 'import_inliner.dart' show ImportInliner; // just for docs. |
| 17 import 'common.dart'; | 18 import 'common.dart'; |
| 18 | 19 |
| 19 /// Combines Dart script tags into a single script tag, and creates a new Dart | 20 /// Combines Dart script tags into a single script tag, and creates a new Dart |
| 20 /// file that calls the main function of each of the original script tags. | 21 /// file that calls the main function of each of the original script tags. |
| 21 /// | 22 /// |
| 22 /// This transformer assumes that all script tags point to external files. To | 23 /// This transformer assumes that all script tags point to external files. To |
| 23 /// support script tags with inlined code, use this transformer after running | 24 /// support script tags with inlined code, use this transformer after running |
| 24 /// [ImportInliner] on an earlier phase. | 25 /// [ImportInliner] on an earlier phase. |
| 25 /// | 26 /// |
| 26 /// Internally, this transformer will convert each script tag into an import | 27 /// Internally, this transformer will convert each script tag into an import |
| 27 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to | 28 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to |
| 28 /// process `@initMethod` and `@CustomTag` annotations in those libraries. | 29 /// process `@initMethod` and `@CustomTag` annotations in those libraries. |
| 29 class ScriptCompactor extends Transformer with PolymerTransformer { | 30 class ScriptCompactor extends Transformer { |
| 30 final TransformOptions options; | 31 final TransformOptions options; |
| 31 | 32 |
| 32 ScriptCompactor(this.options); | 33 ScriptCompactor(this.options); |
| 33 | 34 |
| 34 /// Only run on entry point .html files. | 35 /// Only run on entry point .html files. |
| 35 Future<bool> isPrimary(Asset input) => | 36 Future<bool> isPrimary(Asset input) => |
| 36 new Future.value(options.isHtmlEntryPoint(input.id)); | 37 new Future.value(options.isHtmlEntryPoint(input.id)); |
| 37 | 38 |
| 38 Future apply(Transform transform) { | 39 Future apply(Transform transform) => |
| 39 var id = transform.primaryInput.id; | 40 new _ScriptCompactor(transform, options).apply(); |
| 40 var secondaryId = id.addExtension('.scriptUrls'); | 41 } |
| 41 var logger = transform.logger; | |
| 42 return readPrimaryAsHtml(transform).then((document) { | |
| 43 return transform.readInputAsString(secondaryId).then((libraryIds) { | |
| 44 var libraries = (JSON.decode(libraryIds) as Iterable).map( | |
| 45 (data) => new AssetId.deserialize(data)).toList(); | |
| 46 var mainLibraryId; | |
| 47 var mainScriptTag; | |
| 48 bool changed = false; | |
| 49 | 42 |
| 50 for (var tag in document.querySelectorAll('script')) { | 43 /// Helper class mainly use to flatten the async code. |
| 51 var src = tag.attributes['src']; | 44 class _ScriptCompactor extends PolymerTransformer { |
|
Jennifer Messerly
2014/03/06 22:54:17
wow, this is so much more readable
| |
| 52 if (src == 'packages/polymer/boot.js') { | 45 final TransformOptions options; |
| 53 tag.remove(); | 46 final Transform transform; |
| 54 continue; | 47 final TransformLogger logger; |
| 55 } | 48 final AssetId docId; |
| 56 if (tag.attributes['type'] != 'application/dart') continue; | 49 final AssetId bootstrapId; |
| 57 if (src == null) { | |
| 58 logger.warning('unexpected script without a src url. The ' | |
| 59 'ScriptCompactor transformer should run after running the ' | |
| 60 'InlineCodeExtractor', span: tag.sourceSpan); | |
| 61 continue; | |
| 62 } | |
| 63 if (mainLibraryId != null) { | |
| 64 logger.warning('unexpected script. Only one Dart script tag ' | |
| 65 'per document is allowed.', span: tag.sourceSpan); | |
| 66 tag.remove(); | |
| 67 continue; | |
| 68 } | |
| 69 mainLibraryId = resolve(id, src, logger, tag.sourceSpan); | |
| 70 mainScriptTag = tag; | |
| 71 } | |
| 72 | 50 |
| 73 if (mainScriptTag == null) { | 51 Document document; |
| 74 // We didn't find any main library, nothing to do. | 52 List<AssetId> entryLibraries; |
| 75 transform.addOutput(transform.primaryInput); | 53 AssetId mainLibraryId; |
| 76 return null; | 54 Element mainScriptTag; |
| 77 } | 55 final Map<AssetId, List<_Initializer>> initializers = {}; |
| 78 | 56 |
| 79 // Emit the bootstrap .dart file | 57 _ScriptCompactor(Transform transform, this.options) |
| 80 var bootstrapId = id.addExtension('_bootstrap.dart'); | 58 : transform = transform, |
| 81 mainScriptTag.attributes['src'] = | 59 logger = transform.logger, |
| 82 path.url.basename(bootstrapId.path); | 60 docId = transform.primaryInput.id, |
| 61 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart'); | |
| 83 | 62 |
| 84 libraries.add(mainLibraryId); | 63 Future apply() => |
| 85 var urls = libraries.map((id) => assetUrlFor(id, bootstrapId, logger)) | 64 _loadDocument() |
| 86 .where((url) => url != null).toList(); | 65 .then(_loadEntryLibraries) |
|
Siggi Cherem (dart-lang)
2014/03/06 22:45:23
one subtle, probably not relevant, difference: thi
| |
| 87 var buffer = new StringBuffer()..writeln(MAIN_HEADER); | 66 .then(_processHtml) |
| 88 int i = 0; | 67 .then(_emitNewEntrypoint); |
| 89 for (; i < urls.length; i++) { | |
| 90 buffer.writeln("import '${urls[i]}' as i$i;"); | |
| 91 } | |
| 92 | 68 |
| 93 buffer..write('\n') | 69 /// Loads the primary input as an html document. |
| 94 ..writeln('void main() {') | 70 Future _loadDocument() => |
| 95 ..writeln(' configureForDeployment(['); | 71 readPrimaryAsHtml(transform).then((doc) { document = doc; }); |
| 96 | 72 |
| 97 // Inject @CustomTag and @initMethod initializations for each library | 73 /// Populates [entryLibraries] as a list containing the asset ids of each |
| 98 // that is sourced in a script tag. | 74 /// library loaded on a script tag. The actual work of computing this is done |
| 99 i = 0; | 75 /// in an earlier phase and emited in the `entrypoint.scriptUrls` asset. |
| 100 return Future.forEach(libraries, (lib) { | 76 Future _loadEntryLibraries(_) => |
| 101 return _initializersOf(lib, transform, logger).then((initializers) { | 77 transform.readInputAsString(docId.addExtension('.scriptUrls')) |
| 102 for (var init in initializers) { | 78 .then((libraryIds) { |
|
Jennifer Messerly
2014/03/06 22:54:17
IIRC when I asked the Pub folks, the way they do t
Siggi Cherem (dart-lang)
2014/03/06 23:09:58
interesting. I might prefer the style I had here,
| |
| 103 var code = init.asCode('i$i'); | 79 entryLibraries = (JSON.decode(libraryIds) as Iterable) |
| 104 buffer.write(" $code,\n"); | 80 .map((data) => new AssetId.deserialize(data)).toList(); |
| 105 } | 81 }); |
| 106 i++; | |
| 107 }); | |
| 108 }).then((_) { | |
| 109 buffer..writeln(' ]);') | |
| 110 ..writeln(' i${urls.length - 1}.main();') | |
| 111 ..writeln('}'); | |
| 112 | 82 |
| 113 transform.addOutput(new Asset.fromString( | 83 /// Removes unnecessary script tags, and identifies the main entry point Dart |
| 114 bootstrapId, buffer.toString())); | 84 /// script tag (if any). |
| 115 transform.addOutput(new Asset.fromString(id, document.outerHtml)); | 85 void _processHtml(_) { |
| 116 }); | 86 for (var tag in document.querySelectorAll('script')) { |
| 87 var src = tag.attributes['src']; | |
| 88 if (src == 'packages/polymer/boot.js') { | |
| 89 tag.remove(); | |
| 90 continue; | |
| 91 } | |
| 92 if (tag.attributes['type'] != 'application/dart') continue; | |
| 93 if (src == null) { | |
| 94 logger.warning('unexpected script without a src url. The ' | |
| 95 'ScriptCompactor transformer should run after running the ' | |
| 96 'InlineCodeExtractor', span: tag.sourceSpan); | |
| 97 continue; | |
| 98 } | |
| 99 if (mainLibraryId != null) { | |
| 100 logger.warning('unexpected script. Only one Dart script tag ' | |
| 101 'per document is allowed.', span: tag.sourceSpan); | |
| 102 tag.remove(); | |
| 103 continue; | |
| 104 } | |
| 105 mainLibraryId = resolve(docId, src, logger, tag.sourceSpan); | |
| 106 mainScriptTag = tag; | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 /// Emits the main HTML and Dart bootstrap code for the application. If there | |
| 111 /// were not Dart entry point files, then this simply emits the original HTML. | |
| 112 Future _emitNewEntrypoint(_) { | |
| 113 if (mainScriptTag == null) { | |
| 114 // We didn't find any main library, nothing to do. | |
| 115 transform.addOutput(transform.primaryInput); | |
| 116 return null; | |
| 117 } | |
| 118 | |
| 119 // Emit the bootstrap .dart file | |
| 120 mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path); | |
| 121 entryLibraries.add(mainLibraryId); | |
| 122 return _computeInitializers().then(_createBootstrapCode).then((code) { | |
| 123 transform.addOutput(new Asset.fromString(bootstrapId, code)); | |
| 124 transform.addOutput(new Asset.fromString(docId, document.outerHtml)); | |
| 125 }); | |
| 126 } | |
| 127 | |
| 128 /// Emits the actual bootstrap code. | |
| 129 String _createBootstrapCode(_) { | |
| 130 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | |
| 131 for (int i = 0; i < entryLibraries.length; i++) { | |
| 132 var url = assetUrlFor(entryLibraries[i], bootstrapId, logger); | |
| 133 if (url != null) code.writeln("import '$url' as i$i;"); | |
| 134 } | |
| 135 | |
| 136 code..write('\n') | |
| 137 ..writeln('void main() {') | |
| 138 ..writeln(' configureForDeployment(['); | |
| 139 | |
| 140 // Inject @CustomTag and @initMethod initializations for each library | |
| 141 // that is sourced in a script tag. | |
| 142 for (int i = 0; i < entryLibraries.length; i++) { | |
| 143 for (var init in initializers[entryLibraries[i]]) { | |
| 144 var initCode = init.asCode('i$i'); | |
| 145 code.write(" $initCode,\n"); | |
| 146 } | |
| 147 } | |
| 148 code..writeln(' ]);') | |
| 149 ..writeln(' i${entryLibraries.length - 1}.main();') | |
| 150 ..writeln('}'); | |
| 151 return code.toString(); | |
| 152 } | |
| 153 | |
| 154 /// Computes initializers needed for each library in [entryLibraries]. Results | |
| 155 /// are available afterwards in [initializers]. | |
| 156 Future _computeInitializers() => Future.forEach(entryLibraries, (lib) { | |
| 157 return _initializersOf(lib).then((res) { | |
| 158 initializers[lib] = res; | |
| 117 }); | 159 }); |
| 118 }); | 160 }); |
| 119 } | |
| 120 | 161 |
| 121 /// Computes the initializers of [dartLibrary]. That is, a closure that calls | 162 /// Computes the initializers of [dartLibrary]. That is, a closure that calls |
| 122 /// Polymer.register for each @CustomTag, and any public top-level methods | 163 /// Polymer.register for each @CustomTag, and any public top-level methods |
| 123 /// labeled with @initMethod. | 164 /// labeled with @initMethod. |
| 124 Future<List<_Initializer>> _initializersOf( | 165 Future<List<_Initializer>> _initializersOf(AssetId dartLibrary) { |
|
Siggi Cherem (dart-lang)
2014/03/06 22:45:23
I considered fixing more below, but I might be cha
| |
| 125 AssetId dartLibrary, Transform transform, TransformLogger logger) { | 166 var result = []; |
| 126 var initializers = []; | |
| 127 return transform.readInputAsString(dartLibrary).then((code) { | 167 return transform.readInputAsString(dartLibrary).then((code) { |
| 128 var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); | 168 var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); |
| 129 var unit = parseCompilationUnit(code); | 169 var unit = parseCompilationUnit(code); |
| 130 | 170 |
| 131 return Future.forEach(unit.directives, (directive) { | 171 return Future.forEach(unit.directives, (directive) { |
| 132 // Include anything from parts. | 172 // Include anything from parts. |
| 133 if (directive is PartDirective) { | 173 if (directive is PartDirective) { |
| 134 var targetId = resolve(dartLibrary, directive.uri.stringValue, | 174 var targetId = resolve(dartLibrary, directive.uri.stringValue, |
| 135 logger, _getSpan(file, directive)); | 175 logger, _getSpan(file, directive)); |
| 136 return _initializersOf(targetId, transform, logger) | 176 return _initializersOf(targetId).then(result.addAll); |
| 137 .then(initializers.addAll); | |
| 138 } | 177 } |
| 139 | 178 |
| 140 // Similarly, include anything from exports except what's filtered by | 179 // Similarly, include anything from exports except what's filtered by |
| 141 // the show/hide combinators. | 180 // the show/hide combinators. |
| 142 if (directive is ExportDirective) { | 181 if (directive is ExportDirective) { |
| 143 var targetId = resolve(dartLibrary, directive.uri.stringValue, | 182 var targetId = resolve(dartLibrary, directive.uri.stringValue, |
| 144 logger, _getSpan(file, directive)); | 183 logger, _getSpan(file, directive)); |
| 145 return _initializersOf(targetId, transform, logger) | 184 return _initializersOf(targetId).then( |
| 146 .then((r) => _processExportDirective(directive, r, initializers)); | 185 (r) => _processExportDirective(directive, r, result)); |
| 147 } | 186 } |
| 148 }).then((_) { | 187 }).then((_) { |
| 149 // Scan the code for classes and top-level functions. | 188 // Scan the code for classes and top-level functions. |
| 150 for (var node in unit.declarations) { | 189 for (var node in unit.declarations) { |
| 151 if (node is ClassDeclaration) { | 190 if (node is ClassDeclaration) { |
| 152 _processClassDeclaration(node, initializers, file, logger); | 191 _processClassDeclaration(node, result, file, logger); |
| 153 } else if (node is FunctionDeclaration && | 192 } else if (node is FunctionDeclaration && |
| 154 node.metadata.any(_isInitMethodAnnotation)) { | 193 node.metadata.any(_isInitMethodAnnotation)) { |
| 155 _processFunctionDeclaration(node, initializers, file, logger); | 194 _processFunctionDeclaration(node, result, file, logger); |
| 156 } | 195 } |
| 157 } | 196 } |
| 158 return initializers; | 197 return result; |
| 159 }); | 198 }); |
| 160 }); | 199 }); |
| 161 } | 200 } |
| 162 | 201 |
| 163 static String _simpleUriForSource(AssetId source) => | 202 static String _simpleUriForSource(AssetId source) => |
| 164 source.path.startsWith('lib/') | 203 source.path.startsWith('lib/') |
| 165 ? 'package:${source.package}/${source.path.substring(4)}' : source.path; | 204 ? 'package:${source.package}/${source.path.substring(4)}' : source.path; |
| 166 | 205 |
| 167 /// Filter [exportedInitializers] according to [directive]'s show/hide | 206 /// Filter [exportedInitializers] according to [directive]'s show/hide |
| 168 /// combinators and add the result to [initializers]. | 207 /// combinators and add the result to [result]. |
| 169 // TODO(sigmund): call the analyzer's resolver instead? | 208 // TODO(sigmund): call the analyzer's resolver instead? |
| 170 static _processExportDirective(ExportDirective directive, | 209 static _processExportDirective(ExportDirective directive, |
| 171 List<_Initializer> exportedInitializers, | 210 List<_Initializer> exportedInitializers, |
| 172 List<_Initializer> initializers) { | 211 List<_Initializer> result) { |
| 173 for (var combinator in directive.combinators) { | 212 for (var combinator in directive.combinators) { |
| 174 if (combinator is ShowCombinator) { | 213 if (combinator is ShowCombinator) { |
| 175 var show = combinator.shownNames.map((n) => n.name).toSet(); | 214 var show = combinator.shownNames.map((n) => n.name).toSet(); |
| 176 exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); | 215 exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); |
| 177 } else if (combinator is HideCombinator) { | 216 } else if (combinator is HideCombinator) { |
| 178 var hide = combinator.hiddenNames.map((n) => n.name).toSet(); | 217 var hide = combinator.hiddenNames.map((n) => n.name).toSet(); |
| 179 exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); | 218 exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); |
| 180 } | 219 } |
| 181 } | 220 } |
| 182 initializers.addAll(exportedInitializers); | 221 result.addAll(exportedInitializers); |
| 183 } | 222 } |
| 184 | 223 |
| 185 /// Add an initializer to register [node] as a polymer element if it contains | 224 /// Add an initializer to register [node] as a polymer element if it contains |
| 186 /// an appropriate [CustomTag] annotation. | 225 /// an appropriate [CustomTag] annotation. |
| 187 static _processClassDeclaration(ClassDeclaration node, | 226 static _processClassDeclaration(ClassDeclaration node, |
| 188 List<_Initializer> initializers, SourceFile file, | 227 List<_Initializer> result, SourceFile file, |
| 189 TransformLogger logger) { | 228 TransformLogger logger) { |
| 190 for (var meta in node.metadata) { | 229 for (var meta in node.metadata) { |
| 191 if (!_isCustomTagAnnotation(meta)) continue; | 230 if (!_isCustomTagAnnotation(meta)) continue; |
| 192 var args = meta.arguments.arguments; | 231 var args = meta.arguments.arguments; |
| 193 if (args == null || args.length == 0) { | 232 if (args == null || args.length == 0) { |
| 194 logger.error('Missing argument in @CustomTag annotation', | 233 logger.error('Missing argument in @CustomTag annotation', |
| 195 span: _getSpan(file, meta)); | 234 span: _getSpan(file, meta)); |
| 196 continue; | 235 continue; |
| 197 } | 236 } |
| 198 | 237 |
| 199 var tagName = args[0].stringValue; | 238 var tagName = args[0].stringValue; |
| 200 var typeName = node.name.name; | 239 var typeName = node.name.name; |
| 201 if (typeName.startsWith('_')) { | 240 if (typeName.startsWith('_')) { |
| 202 logger.error('@CustomTag is no longer supported on private ' | 241 logger.error('@CustomTag is no longer supported on private ' |
| 203 'classes: $tagName', span: _getSpan(file, node.name)); | 242 'classes: $tagName', span: _getSpan(file, node.name)); |
| 204 continue; | 243 continue; |
| 205 } | 244 } |
| 206 initializers.add(new _CustomTagInitializer(tagName, typeName)); | 245 result.add(new _CustomTagInitializer(tagName, typeName)); |
| 207 } | 246 } |
| 208 } | 247 } |
| 209 | 248 |
| 210 /// Add a method initializer for [function]. | 249 /// Add a method initializer for [function]. |
| 211 static _processFunctionDeclaration(FunctionDeclaration function, | 250 static _processFunctionDeclaration(FunctionDeclaration function, |
| 212 List<_Initializer> initializers, SourceFile file, | 251 List<_Initializer> result, SourceFile file, |
| 213 TransformLogger logger) { | 252 TransformLogger logger) { |
| 214 var name = function.name.name; | 253 var name = function.name.name; |
| 215 if (name.startsWith('_')) { | 254 if (name.startsWith('_')) { |
| 216 logger.error('@initMethod is no longer supported on private ' | 255 logger.error('@initMethod is no longer supported on private ' |
| 217 'functions: $name', span: _getSpan(file, function.name)); | 256 'functions: $name', span: _getSpan(file, function.name)); |
| 218 return; | 257 return; |
| 219 } | 258 } |
| 220 initializers.add(new _InitMethodInitializer(name)); | 259 result.add(new _InitMethodInitializer(name)); |
| 221 } | 260 } |
| 222 } | 261 } |
| 223 | 262 |
| 224 // TODO(sigmund): consider support for importing annotations with prefixes. | 263 // TODO(sigmund): consider support for importing annotations with prefixes. |
| 225 bool _isInitMethodAnnotation(Annotation node) => | 264 bool _isInitMethodAnnotation(Annotation node) => |
| 226 node.name.name == 'initMethod' && node.constructorName == null && | 265 node.name.name == 'initMethod' && node.constructorName == null && |
| 227 node.arguments == null; | 266 node.arguments == null; |
| 228 bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; | 267 bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; |
| 229 | 268 |
| 230 abstract class _Initializer { | 269 abstract class _Initializer { |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 249 String asCode(String prefix) => | 288 String asCode(String prefix) => |
| 250 "() => Polymer.register('$tagName', $prefix.$typeName)"; | 289 "() => Polymer.register('$tagName', $prefix.$typeName)"; |
| 251 } | 290 } |
| 252 | 291 |
| 253 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 292 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
| 254 | 293 |
| 255 const MAIN_HEADER = """ | 294 const MAIN_HEADER = """ |
| 256 library app_bootstrap; | 295 library app_bootstrap; |
| 257 | 296 |
| 258 import 'package:polymer/polymer.dart'; | 297 import 'package:polymer/polymer.dart'; |
| 298 import 'package:smoke/static.dart' as smoke; | |
| 259 """; | 299 """; |
| OLD | NEW |