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; |
| 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:html5lib/dom.dart' show Document, Element, Text; |
| 12 import 'package:html5lib/dom_parsing.dart'; | |
| 12 import 'package:analyzer/src/generated/ast.dart'; | 13 import 'package:analyzer/src/generated/ast.dart'; |
| 14 import 'package:analyzer/src/generated/element.dart' hide Element; | |
| 15 import 'package:analyzer/src/generated/element.dart' as analyzer show Element; | |
| 13 import 'package:barback/barback.dart'; | 16 import 'package:barback/barback.dart'; |
| 14 import 'package:code_transformers/assets.dart'; | 17 import 'package:code_transformers/assets.dart'; |
| 15 import 'package:path/path.dart' as path; | 18 import 'package:path/path.dart' as path; |
| 16 import 'package:source_maps/span.dart' show SourceFile; | 19 import 'package:source_maps/span.dart' show SourceFile; |
| 20 import 'package:smoke/codegen/generator.dart'; | |
| 21 import 'package:smoke/codegen/recorder.dart'; | |
| 22 import 'package:code_transformers/resolver.dart'; | |
| 23 import 'package:code_transformers/src/dart_sdk.dart'; | |
| 24 | |
| 25 import 'package:polymer_expressions/expression.dart' as pe; | |
| 26 import 'package:polymer_expressions/parser.dart' as pe; | |
| 27 import 'package:polymer_expressions/visitor.dart' as pe; | |
| 17 | 28 |
| 18 import 'import_inliner.dart' show ImportInliner; // just for docs. | 29 import 'import_inliner.dart' show ImportInliner; // just for docs. |
| 19 import 'common.dart'; | 30 import 'common.dart'; |
| 20 | 31 |
| 21 /// Combines Dart script tags into a single script tag, and creates a new Dart | 32 /// Combines Dart script tags into a single script tag, and creates a new Dart |
| 22 /// file that calls the main function of each of the original script tags. | 33 /// file that calls the main function of each of the original script tags. |
| 23 /// | 34 /// |
| 24 /// This transformer assumes that all script tags point to external files. To | 35 /// This transformer assumes that all script tags point to external files. To |
| 25 /// support script tags with inlined code, use this transformer after running | 36 /// support script tags with inlined code, use this transformer after running |
| 26 /// [ImportInliner] on an earlier phase. | 37 /// [ImportInliner] on an earlier phase. |
| 27 /// | 38 /// |
| 28 /// Internally, this transformer will convert each script tag into an import | 39 /// Internally, this transformer will convert each script tag into an import |
| 29 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to | 40 /// statement to a library, and then uses `initPolymer` (see polymer.dart) to |
| 30 /// process `@initMethod` and `@CustomTag` annotations in those libraries. | 41 /// process `@initMethod` and `@CustomTag` annotations in those libraries. |
| 31 class ScriptCompactor extends Transformer { | 42 class ScriptCompactor extends Transformer { |
| 43 final Resolvers resolvers; | |
|
Jennifer Messerly
2014/03/27 02:20:32
this is an issue with the code_transformer pkg, bu
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
Yeah, that's a good point.
Just chatted with Pete
| |
| 32 final TransformOptions options; | 44 final TransformOptions options; |
| 33 | 45 |
| 34 ScriptCompactor(this.options); | 46 ScriptCompactor(this.options, {String sdkDir}) |
| 47 : resolvers = new Resolvers(sdkDir != null ? sdkDir : dartSdkDirectory); | |
| 35 | 48 |
| 36 /// Only run on entry point .html files. | 49 /// Only run on entry point .html files. |
| 37 Future<bool> isPrimary(Asset input) => | 50 Future<bool> isPrimary(Asset input) => |
| 38 new Future.value(options.isHtmlEntryPoint(input.id)); | 51 new Future.value(options.isHtmlEntryPoint(input.id)); |
| 39 | 52 |
| 40 Future apply(Transform transform) => | 53 Future apply(Transform transform) => |
| 41 new _ScriptCompactor(transform, options).apply(); | 54 new _ScriptCompactor(transform, options, resolvers).apply(); |
| 42 } | 55 } |
| 43 | 56 |
| 44 /// Helper class mainly use to flatten the async code. | 57 /// Helper class mainly use to flatten the async code. |
| 45 class _ScriptCompactor extends PolymerTransformer { | 58 class _ScriptCompactor extends PolymerTransformer { |
| 46 final TransformOptions options; | 59 final TransformOptions options; |
| 47 final Transform transform; | 60 final Transform transform; |
| 48 final TransformLogger logger; | 61 final TransformLogger logger; |
| 49 final AssetId docId; | 62 final AssetId docId; |
| 50 final AssetId bootstrapId; | 63 final AssetId bootstrapId; |
| 51 | 64 |
| 65 /// HTML document parsed from [docId]. | |
| 52 Document document; | 66 Document document; |
| 67 | |
| 68 /// List of ids for each Dart entry script tag (the main tag and any tag | |
| 69 /// included on each custom element definition). | |
| 53 List<AssetId> entryLibraries; | 70 List<AssetId> entryLibraries; |
| 71 | |
| 72 /// The id of the main Dart program. | |
| 54 AssetId mainLibraryId; | 73 AssetId mainLibraryId; |
| 74 | |
| 75 /// Script tag that loads the Dart entry point. | |
| 55 Element mainScriptTag; | 76 Element mainScriptTag; |
| 56 final Map<AssetId, List<_Initializer>> initializers = {}; | |
| 57 | 77 |
| 58 _ScriptCompactor(Transform transform, this.options) | 78 /// Initializers that will register custom tags or invoke `initMethod`s. |
| 79 final List<_Initializer> initializers = []; | |
| 80 | |
| 81 /// Attributes published on a custom-tag. We make these available via | |
| 82 /// reflection even if @published was not used. | |
| 83 final Map<String, List<String>> publishedAttributes = {}; | |
| 84 | |
| 85 /// Those custom-tags for which we have found a corresponding class in code. | |
| 86 final Set<String> tagsWithClasses = new Set(); | |
| 87 | |
| 88 /// Hook needed to access the analyzer within barback transformers. | |
| 89 final Resolvers resolvers; | |
| 90 | |
| 91 /// The resolver instance associated with a single run of this transformer. | |
| 92 Resolver resolver; | |
| 93 | |
| 94 /// Element representing `HtmlElement`. | |
| 95 ClassElement _htmlElementElement; | |
|
Jennifer Messerly
2014/03/27 02:20:32
A thought here: there's a lot of state in this cla
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
good idea. I moved these out in to a separate clas
| |
| 96 | |
| 97 /// Element representing the constructor of `@CustomTag`. | |
| 98 ConstructorElement _customTagConstructor; | |
| 99 | |
| 100 /// Element representing the type of `@published`. | |
| 101 ClassElement _publishedElement; | |
| 102 | |
| 103 /// Element representing the type of `@observable`. | |
| 104 ClassElement _observableElement; | |
| 105 | |
| 106 /// Element representing the type of `@ObserveProperty`. | |
| 107 ClassElement _observePropertyElement; | |
| 108 | |
| 109 /// Element representing the `@initMethod` annotation. | |
| 110 TopLevelVariableElement _initMethodElement; | |
| 111 | |
| 112 /// Code generator used to create the static initialization for smoke. | |
| 113 final SmokeCodeGenerator generator = new SmokeCodeGenerator(); | |
|
Jennifer Messerly
2014/03/27 02:20:32
IMO, it's nice to not repeat the type annotation f
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
Done.
It seems fine in this particular example, b
| |
| 114 | |
| 115 /// Recorder that uses analyzer data to feed data to [generator]. | |
| 116 Recorder recorder; | |
| 117 | |
| 118 _ScriptCompactor(Transform transform, this.options, this.resolvers) | |
| 59 : transform = transform, | 119 : transform = transform, |
| 60 logger = transform.logger, | 120 logger = transform.logger, |
| 61 docId = transform.primaryInput.id, | 121 docId = transform.primaryInput.id, |
| 62 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart'); | 122 bootstrapId = transform.primaryInput.id.addExtension('_bootstrap.dart'); |
| 63 | 123 |
| 64 Future apply() => | 124 Future apply() => |
| 65 _loadDocument() | 125 _loadDocument() |
| 66 .then(_loadEntryLibraries) | 126 .then(_loadEntryLibraries) |
| 67 .then(_processHtml) | 127 .then(_processHtml) |
| 68 .then(_emitNewEntrypoint); | 128 .then(_emitNewEntrypoint); |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 113 Future _emitNewEntrypoint(_) { | 173 Future _emitNewEntrypoint(_) { |
| 114 if (mainScriptTag == null) { | 174 if (mainScriptTag == null) { |
| 115 // We didn't find any main library, nothing to do. | 175 // We didn't find any main library, nothing to do. |
| 116 transform.addOutput(transform.primaryInput); | 176 transform.addOutput(transform.primaryInput); |
| 117 return null; | 177 return null; |
| 118 } | 178 } |
| 119 | 179 |
| 120 // Emit the bootstrap .dart file | 180 // Emit the bootstrap .dart file |
| 121 mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path); | 181 mainScriptTag.attributes['src'] = path.url.basename(bootstrapId.path); |
| 122 entryLibraries.add(mainLibraryId); | 182 entryLibraries.add(mainLibraryId); |
| 123 return _computeInitializers().then(_createBootstrapCode).then((code) { | 183 |
| 124 transform.addOutput(new Asset.fromString(bootstrapId, code)); | 184 return _initResolver() |
| 125 transform.addOutput(new Asset.fromString(docId, document.outerHtml)); | 185 .then(_extractUsesOfMirrors) |
| 186 .then(_emitFiles) | |
| 187 .then((_) => resolver.release()); | |
| 188 } | |
| 189 | |
| 190 /// Load a resolver that computes information for every library in | |
| 191 /// [entryLibraries], then use it to initialize the [recorder] (for import | |
| 192 /// resolution) and to resolve specific elements (for analyzing the user's | |
| 193 /// code). | |
| 194 Future _initResolver() => resolvers.get(transform, entryLibraries).then((r) { | |
| 195 resolver = r; | |
| 196 recorder = new Recorder(generator, | |
| 197 (lib) => resolver.getImportUri(lib, from: bootstrapId).toString()); | |
| 198 | |
| 199 // Load class elements that are used in queries for codegen. | |
| 200 var polymerLib = r.getLibrary(new AssetId('polymer', 'lib/polymer.dart')); | |
|
Jennifer Messerly
2014/03/27 02:20:32
just curious, any reason to prefer AssetIds over p
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
code_transformers indexes the libraries that are r
| |
| 201 if (polymerLib == null) _definitionError('the polymer library'); | |
| 202 var htmlLib = r.getLibraryByUri(Uri.parse('dart:html')); | |
| 203 if (htmlLib == null) _definitionError('the "dart:html" library'); | |
| 204 var observeLib = r.getLibrary( | |
| 205 new AssetId('observe', 'lib/src/metadata.dart')); | |
| 206 if (observeLib == null) _definitionError('the observe library'); | |
| 207 | |
| 208 for (var unit in polymerLib.parts) { | |
| 209 if (unit.uri == 'src/loader.dart') { | |
| 210 _initMethodElement = unit.topLevelVariables.firstWhere( | |
| 211 (t) => t.displayName == 'initMethod'); | |
| 212 break; | |
| 213 } | |
| 214 } | |
| 215 _customTagConstructor = | |
| 216 _lookupType(polymerLib, 'CustomTag').constructors.first; | |
| 217 _publishedElement = _lookupType(polymerLib, 'PublishedProperty'); | |
| 218 _observableElement = _lookupType(observeLib, 'ObservableProperty'); | |
| 219 _observePropertyElement = _lookupType(polymerLib, 'ObserveProperty'); | |
| 220 _htmlElementElement = _lookupType(htmlLib, 'HtmlElement'); | |
| 221 if (_initMethodElement == null) _definitionError('@initMethod'); | |
| 222 }); | |
| 223 | |
| 224 _lookupType(LibraryElement lib, String typeName) { | |
| 225 var result = lib.getType(typeName); | |
| 226 if (result == null) _definitionError(typeName); | |
| 227 return result; | |
| 228 } | |
| 229 | |
| 230 _definitionError(name) { | |
| 231 throw new StateError("Internal error in polymer-builder: couldn't find " | |
|
Jennifer Messerly
2014/03/27 02:20:32
could this error be cased by the user forgetting t
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
It was happening in our tests before I added the m
| |
| 232 "definition of $name."); | |
| 233 } | |
| 234 | |
| 235 /// Inspects the entire program to find out anything that polymer accesses | |
| 236 /// using mirrors and produces static information that can be used to replace | |
| 237 /// the mirror-based loader and the uses of mirrors through the `smoke` | |
| 238 /// package. This includes: | |
| 239 /// | |
| 240 /// * visiting entry-libraries to extract initializers, | |
| 241 /// * visiting polymer-expressions to extract getters and setters, | |
| 242 /// * looking for published fields of custom elements, and | |
| 243 /// * looking for event handlers and callbacks of change notifications. | |
| 244 /// | |
| 245 void _extractUsesOfMirrors(_) { | |
|
Jennifer Messerly
2014/03/27 02:20:32
Just wanted to say this method is awesome. It read
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
:)
| |
| 246 // Generate getters and setters needed to evaluate polymer expressions, and | |
| 247 // extract information about published attributes. | |
| 248 new _HtmlExtractor(generator, publishedAttributes).visit(document); | |
| 249 | |
| 250 // Process all classes and top-level functions to include initializers, | |
| 251 // register custom elements, and include special fields and methods in | |
| 252 // custom element classes. | |
| 253 for (var id in entryLibraries) { | |
| 254 var lib = resolver.getLibrary(id); | |
| 255 for (var cls in _visibleClassesOf(lib)) { | |
| 256 _processClass(cls, id); | |
| 257 } | |
| 258 | |
| 259 for (var fun in _visibleTopLevelMethodsOf(lib)) { | |
| 260 _processFunction(fun, id); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 // Warn about tagNames with no corresponding Dart class | |
| 265 // TODO(sigmund): is there a way to exclude polymer.js elements? should we | |
| 266 // remove the warning below? | |
|
Jennifer Messerly
2014/03/27 02:20:32
yes, please, let's remove :)
(even for Dart, thin
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
Done.
| |
| 267 publishedAttributes.forEach((tagName, attrs) { | |
| 268 if (tagsWithClasses.contains(tagName)) return; | |
| 269 logger.warning('Class for custom-element "$tagName" not found. ' | |
| 270 'Code-generation might be incomplete.'); | |
| 271 // We include accessors for the missing attributes because they help get | |
| 272 // better warning messages at runtime. | |
| 273 for (var attr in attrs) { | |
| 274 generator.addGetter(attr); | |
| 275 generator.addSetter(attr); | |
| 276 generator.addSymbol(attr); | |
| 277 } | |
| 126 }); | 278 }); |
| 127 } | 279 } |
| 128 | 280 |
| 129 /// Emits the actual bootstrap code. | 281 /// Retrieves all classses that are visible if you were to import [lib]. This |
| 130 String _createBootstrapCode(_) { | 282 /// includes exported classes from other libraries. |
| 283 List<ClassElement> _visibleClassesOf(LibraryElement lib) { | |
| 284 var result = []; | |
| 285 result.addAll(lib.units.expand((u) => u.types)); | |
| 286 for (var e in lib.exports) { | |
| 287 var exported = e.exportedLibrary.units.expand((u) => u.types).toList(); | |
| 288 _filter(exported, e.combinators); | |
| 289 result.addAll(exported); | |
| 290 } | |
| 291 return result; | |
| 292 } | |
| 293 | |
| 294 /// Retrieves all top-level methods that are visible if you were to import | |
| 295 /// [lib]. This includes exported methods from other libraries too. | |
| 296 List<ClassElement> _visibleTopLevelMethodsOf(LibraryElement lib) { | |
| 297 var result = []; | |
| 298 result.addAll(lib.units.expand((u) => u.functions)); | |
| 299 for (var e in lib.exports) { | |
| 300 var exported = e.exportedLibrary.units | |
| 301 .expand((u) => u.functions).toList(); | |
| 302 _filter(exported, e.combinators); | |
| 303 result.addAll(exported); | |
| 304 } | |
| 305 return result; | |
| 306 } | |
| 307 | |
| 308 /// Filters [elements] that come from an export, according to its show/hide | |
| 309 /// combinators. This modifies [elements] in place. | |
| 310 void _filter(List<analyzer.Element> elements, | |
| 311 List<NamespaceCombinator> combinators) { | |
| 312 for (var c in combinators) { | |
| 313 if (c is ShowElementCombinator) { | |
| 314 var show = c.shownNames.toSet(); | |
| 315 elements.retainWhere((e) => show.contains(e.displayName)); | |
| 316 } else if (c is HideElementCombinator) { | |
| 317 var hide = c.hiddenNames.toSet(); | |
| 318 elements.removeWhere((e) => hide.contains(e.displayName)); | |
| 319 } | |
| 320 } | |
| 321 } | |
| 322 | |
| 323 /// Process a class ([cls]). If it contains an appropriate [CustomTag] | |
|
Jennifer Messerly
2014/03/27 02:20:32
so, today I can have a Dart class, mark it @reflec
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
This is a great idea! I made the switch.
The only
Jennifer Messerly
2014/03/28 21:48:35
ah, yeah that makes sense. seems like several prob
| |
| 324 /// annotation, we include an initializer to register this class, and make | |
| 325 /// sure to include everything that might be accessed or queried from them | |
| 326 /// using the smoke package. In particular, polymer uses smoke for the | |
| 327 /// following: | |
| 328 /// * invoke #registerCallback on custom elements classes, if present. | |
| 329 /// * query for methods ending in `*Changed`. | |
| 330 /// * query for methods with the `@ObserveProperty` annotation. | |
| 331 /// * query for non-final properties labeled with `@published`. | |
| 332 /// * read declarations of properties named in the `attributes` attribute. | |
| 333 /// * read/write the value of published properties . | |
| 334 /// * invoke methods in event handlers. | |
| 335 _processClass(ClassElement cls, AssetId id) { | |
| 336 // Check whether the class has a @CustomTag annotation. Typically we expect | |
| 337 // a single @CustomTag, but it's possible to have several. | |
| 338 var tagNames = []; | |
| 339 for (var meta in cls.node.metadata) { | |
| 340 var tagName = _extractTagName(meta, cls); | |
| 341 if (tagName != null) tagNames.add(tagName); | |
| 342 } | |
| 343 if (tagNames.isEmpty) return; | |
| 344 | |
| 345 if (cls.isPrivate) { | |
| 346 logger.error('@CustomTag is no longer supported on private classes:' | |
|
Jennifer Messerly
2014/03/27 02:20:32
just curious, is this something we could lift in t
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
(Note that this warning was there before this chan
| |
| 347 ' ${tagNames.first}', span: _spanForNode(cls, cls.node.name)); | |
| 348 return; | |
| 349 } | |
| 350 | |
| 351 // Include #registerCallback if it exists. Note that by default lookupMember | |
| 352 // and query will also add the corresponding getters and setters. | |
| 353 recorder.lookupMember(cls, 'registerCallback'); | |
|
Jennifer Messerly
2014/03/27 02:20:32
this reminds me, #registerCallback needs to go awa
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
it's actually funny because smoke initially didn't
| |
| 354 | |
| 355 // Include methods that end with *Changed. | |
| 356 recorder.runQuery(cls, new QueryOptions( | |
| 357 includeFields: false, includeProperties: false, | |
| 358 includeInherited: true, includeMethods: true, | |
| 359 includeUpTo: _htmlElementElement, | |
| 360 matches: (n) => n.endsWith('Changed') && n != 'attributeChanged')); | |
| 361 | |
| 362 // Include methods marked with @ObserveProperty. | |
| 363 recorder.runQuery(cls, new QueryOptions( | |
| 364 includeFields: false, includeProperties: false, | |
| 365 includeInherited: true, includeMethods: true, | |
| 366 includeUpTo: _htmlElementElement, | |
| 367 withAnnotations: [_observePropertyElement])); | |
| 368 | |
| 369 // Include @published and @observable properties. | |
| 370 // Symbols in @published are used when resolving bindings on published | |
| 371 // attributes, symbols for @observable are used via path observers when | |
| 372 // implementing *Changed an @ObserveProperty. | |
| 373 // TODO(sigmund): consider including only those symbols mentioned in | |
| 374 // *Changed and @ObserveProperty instead. | |
| 375 recorder.runQuery(cls, new QueryOptions(includeUpTo: _htmlElementElement, | |
| 376 withAnnotations: [_publishedElement, _observableElement])); | |
| 377 | |
| 378 for (var tagName in tagNames) { | |
| 379 tagsWithClasses.add(tagName); | |
| 380 // Include an initializer that will call Polymer.register | |
| 381 initializers.add(new _CustomTagInitializer(id, tagName, cls.displayName)); | |
| 382 | |
| 383 // Include also properties published via the `attributes` attribute. | |
| 384 var attrs = publishedAttributes[tagName]; | |
| 385 if (attrs == null) continue; | |
| 386 for (var attr in attrs) { | |
| 387 recorder.lookupMember(cls, attr, recursive: true); | |
| 388 } | |
| 389 } | |
| 390 } | |
| 391 | |
| 392 /// If [meta] is [CustomTag], extract the name associated with the tag. | |
| 393 String _extractTagName(Annotation meta, ClassElement cls) { | |
| 394 if (meta.element != _customTagConstructor) return null; | |
| 395 | |
| 396 // Read argument from the AST | |
| 397 var args = meta.arguments.arguments; | |
| 398 if (args == null || args.length == 0) { | |
| 399 logger.error('Missing argument in @CustomTag annotation', | |
| 400 span: _spanForNode(cls, meta)); | |
| 401 return null; | |
| 402 } | |
| 403 | |
| 404 if (args[0] is! StringLiteral) { | |
| 405 logger.error('Only string literals are currently supported by the polymer' | |
|
Jennifer Messerly
2014/03/27 02:20:32
totally fine to fix in a follow up, but it looks l
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
nice! Done
Jennifer Messerly
2014/03/28 21:48:35
it worked? awesome! :D
Siggi Cherem (dart-lang)
2014/03/28 22:08:47
yeah! I was so surprised that I had to explicitly
| |
| 406 ' transformers. See dartbug.com/17739.', | |
| 407 span: _spanForNode(cls, meta)); | |
| 408 return null; | |
| 409 } | |
| 410 return args[0].stringValue; | |
| 411 } | |
| 412 | |
| 413 /// Adds the top-level [function] as an initalizer if it's marked with | |
| 414 /// `@initMethod`. | |
| 415 _processFunction(FunctionElement function, AssetId id) { | |
| 416 bool initMethodFound = false; | |
| 417 for (var meta in function.metadata) { | |
| 418 var e = meta.element; | |
| 419 if (e is PropertyAccessorElement && e.variable == _initMethodElement) { | |
| 420 initMethodFound = true; | |
| 421 break; | |
| 422 } | |
| 423 } | |
| 424 if (!initMethodFound) return; | |
| 425 if (function.isPrivate) { | |
| 426 logger.error('@initMethod is no longer supported on private ' | |
| 427 'functions: ${function.displayName}', | |
| 428 span: _spanForNode(function, function.node.name)); | |
| 429 return; | |
| 430 } | |
| 431 initializers.add(new _InitMethodInitializer(id, function.displayName)); | |
| 432 } | |
| 433 | |
| 434 /// Writes the final output for the bootstrap Dart file and entrypoint HTML | |
| 435 /// file. | |
| 436 void _emitFiles(_) { | |
| 131 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); | 437 StringBuffer code = new StringBuffer()..writeln(MAIN_HEADER); |
| 132 for (int i = 0; i < entryLibraries.length; i++) { | 438 Map<AssetId, String> prefixes = {}; |
| 133 var url = assetUrlFor(entryLibraries[i], bootstrapId, logger); | 439 int i = 0; |
| 134 if (url != null) code.writeln("import '$url' as i$i;"); | 440 for (var id in entryLibraries) { |
| 135 } | 441 var url = assetUrlFor(id, bootstrapId, logger); |
| 136 | 442 if (url == null) continue; |
| 137 code..write('\n') | 443 code.writeln("import '$url' as i$i;"); |
| 138 ..writeln('void main() {') | 444 prefixes[id] = 'i$i'; |
| 139 ..writeln(' configureForDeployment(['); | 445 i++; |
| 140 | 446 } |
| 141 // Inject @CustomTag and @initMethod initializations for each library | 447 |
| 142 // that is sourced in a script tag. | 448 // Include smoke initialization. |
| 143 for (int i = 0; i < entryLibraries.length; i++) { | 449 generator.writeImports(code); |
| 144 for (var init in initializers[entryLibraries[i]]) { | 450 generator.writeTopLevelDeclarations(code); |
| 145 var initCode = init.asCode('i$i'); | 451 code.writeln('\nvoid main() {'); |
| 146 code.write(" $initCode,\n"); | 452 generator.writeInitCall(code); |
| 147 } | 453 code.writeln(' configureForDeployment(['); |
| 454 | |
| 455 // Include initializers to switch from mirrors_loader to static_loader. | |
| 456 // TODO(sigmund): do we need to sort out initializers to ensure that parent | |
| 457 // classes are initialized first? | |
|
Jennifer Messerly
2014/03/27 02:20:32
currently no -- Polymer.js will handle it using th
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
yeah, we should be preserving the same order here,
| |
| 458 for (var init in initializers) { | |
| 459 var initCode = init.asCode(prefixes[init.assetId]); | |
| 460 code.write(" $initCode,\n"); | |
| 148 } | 461 } |
| 149 code..writeln(' ]);') | 462 code..writeln(' ]);') |
| 150 ..writeln(' i${entryLibraries.length - 1}.main();') | 463 ..writeln(' i${entryLibraries.length - 1}.main();') |
| 151 ..writeln('}'); | 464 ..writeln('}'); |
| 152 return code.toString(); | 465 transform.addOutput(new Asset.fromString(bootstrapId, code.toString())); |
| 153 } | 466 transform.addOutput(new Asset.fromString(docId, document.outerHtml)); |
| 154 | 467 } |
| 155 /// Computes initializers needed for each library in [entryLibraries]. Results | 468 |
| 156 /// are available afterwards in [initializers]. | 469 _spanForNode(analyzer.Element context, AstNode node) { |
| 157 Future _computeInitializers() => Future.forEach(entryLibraries, (lib) { | 470 var file = resolver.getSourceFile(context); |
| 158 return _initializersOf(lib).then((res) { | 471 return file.span(node.offset, node.end); |
| 159 initializers[lib] = res; | |
| 160 }); | |
| 161 }); | |
| 162 | |
| 163 /// Computes the initializers of [dartLibrary]. That is, a closure that calls | |
| 164 /// Polymer.register for each @CustomTag, and any public top-level methods | |
| 165 /// labeled with @initMethod. | |
| 166 Future<List<_Initializer>> _initializersOf(AssetId dartLibrary) { | |
| 167 var result = []; | |
| 168 return transform.readInputAsString(dartLibrary).then((code) { | |
| 169 var file = new SourceFile.text(_simpleUriForSource(dartLibrary), code); | |
| 170 var unit = parseCompilationUnit(code); | |
| 171 | |
| 172 return Future.forEach(unit.directives, (directive) { | |
| 173 // Include anything from parts. | |
| 174 if (directive is PartDirective) { | |
| 175 var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue, | |
| 176 logger, _getSpan(file, directive)); | |
| 177 return _initializersOf(targetId).then(result.addAll); | |
| 178 } | |
| 179 | |
| 180 // Similarly, include anything from exports except what's filtered by | |
| 181 // the show/hide combinators. | |
| 182 if (directive is ExportDirective) { | |
| 183 var targetId = uriToAssetId(dartLibrary, directive.uri.stringValue, | |
| 184 logger, _getSpan(file, directive)); | |
| 185 return _initializersOf(targetId).then( | |
| 186 (r) => _processExportDirective(directive, r, result)); | |
| 187 } | |
| 188 }).then((_) { | |
| 189 // Scan the code for classes and top-level functions. | |
| 190 for (var node in unit.declarations) { | |
| 191 if (node is ClassDeclaration) { | |
| 192 _processClassDeclaration(node, result, file, logger); | |
| 193 } else if (node is FunctionDeclaration && | |
| 194 node.metadata.any(_isInitMethodAnnotation)) { | |
| 195 _processFunctionDeclaration(node, result, file, logger); | |
| 196 } | |
| 197 } | |
| 198 return result; | |
| 199 }); | |
| 200 }); | |
| 201 } | |
| 202 | |
| 203 static String _simpleUriForSource(AssetId source) => | |
| 204 source.path.startsWith('lib/') | |
| 205 ? 'package:${source.package}/${source.path.substring(4)}' : source.path; | |
| 206 | |
| 207 /// Filter [exportedInitializers] according to [directive]'s show/hide | |
| 208 /// combinators and add the result to [result]. | |
| 209 // TODO(sigmund): call the analyzer's resolver instead? | |
| 210 static _processExportDirective(ExportDirective directive, | |
| 211 List<_Initializer> exportedInitializers, | |
| 212 List<_Initializer> result) { | |
| 213 for (var combinator in directive.combinators) { | |
| 214 if (combinator is ShowCombinator) { | |
| 215 var show = combinator.shownNames.map((n) => n.name).toSet(); | |
| 216 exportedInitializers.retainWhere((e) => show.contains(e.symbolName)); | |
| 217 } else if (combinator is HideCombinator) { | |
| 218 var hide = combinator.hiddenNames.map((n) => n.name).toSet(); | |
| 219 exportedInitializers.removeWhere((e) => hide.contains(e.symbolName)); | |
| 220 } | |
| 221 } | |
| 222 result.addAll(exportedInitializers); | |
| 223 } | |
| 224 | |
| 225 /// Add an initializer to register [node] as a polymer element if it contains | |
| 226 /// an appropriate [CustomTag] annotation. | |
| 227 static _processClassDeclaration(ClassDeclaration node, | |
| 228 List<_Initializer> result, SourceFile file, | |
| 229 TransformLogger logger) { | |
| 230 for (var meta in node.metadata) { | |
| 231 if (!_isCustomTagAnnotation(meta)) continue; | |
| 232 var args = meta.arguments.arguments; | |
| 233 if (args == null || args.length == 0) { | |
| 234 logger.error('Missing argument in @CustomTag annotation', | |
| 235 span: _getSpan(file, meta)); | |
| 236 continue; | |
| 237 } | |
| 238 | |
| 239 var tagName = args[0].stringValue; | |
| 240 var typeName = node.name.name; | |
| 241 if (typeName.startsWith('_')) { | |
| 242 logger.error('@CustomTag is no longer supported on private ' | |
| 243 'classes: $tagName', span: _getSpan(file, node.name)); | |
| 244 continue; | |
| 245 } | |
| 246 result.add(new _CustomTagInitializer(tagName, typeName)); | |
| 247 } | |
| 248 } | |
| 249 | |
| 250 /// Add a method initializer for [function]. | |
| 251 static _processFunctionDeclaration(FunctionDeclaration function, | |
| 252 List<_Initializer> result, SourceFile file, | |
| 253 TransformLogger logger) { | |
| 254 var name = function.name.name; | |
| 255 if (name.startsWith('_')) { | |
| 256 logger.error('@initMethod is no longer supported on private ' | |
| 257 'functions: $name', span: _getSpan(file, function.name)); | |
| 258 return; | |
| 259 } | |
| 260 result.add(new _InitMethodInitializer(name)); | |
| 261 } | 472 } |
| 262 } | 473 } |
| 263 | 474 |
| 264 // TODO(sigmund): consider support for importing annotations with prefixes. | |
| 265 bool _isInitMethodAnnotation(Annotation node) => | |
| 266 node.name.name == 'initMethod' && node.constructorName == null && | |
| 267 node.arguments == null; | |
| 268 bool _isCustomTagAnnotation(Annotation node) => node.name.name == 'CustomTag'; | |
| 269 | |
| 270 abstract class _Initializer { | 475 abstract class _Initializer { |
| 476 AssetId get assetId; | |
| 271 String get symbolName; | 477 String get symbolName; |
| 272 String asCode(String prefix); | 478 String asCode(String prefix); |
| 273 } | 479 } |
| 274 | 480 |
| 275 class _InitMethodInitializer implements _Initializer { | 481 class _InitMethodInitializer implements _Initializer { |
| 276 String methodName; | 482 final AssetId assetId; |
| 483 final String methodName; | |
| 277 String get symbolName => methodName; | 484 String get symbolName => methodName; |
| 278 _InitMethodInitializer(this.methodName); | 485 _InitMethodInitializer(this.assetId, this.methodName); |
| 279 | 486 |
| 280 String asCode(String prefix) => "$prefix.$methodName"; | 487 String asCode(String prefix) => "$prefix.$methodName"; |
| 281 } | 488 } |
| 282 | 489 |
| 283 class _CustomTagInitializer implements _Initializer { | 490 class _CustomTagInitializer implements _Initializer { |
| 284 String tagName; | 491 final AssetId assetId; |
| 285 String typeName; | 492 final String tagName; |
| 493 final String typeName; | |
| 286 String get symbolName => typeName; | 494 String get symbolName => typeName; |
| 287 _CustomTagInitializer(this.tagName, this.typeName); | 495 _CustomTagInitializer(this.assetId, this.tagName, this.typeName); |
| 288 | 496 |
| 289 String asCode(String prefix) => | 497 String asCode(String prefix) => |
| 290 "() => Polymer.register('$tagName', $prefix.$typeName)"; | 498 "() => Polymer.register('$tagName', $prefix.$typeName)"; |
| 291 } | 499 } |
| 292 | 500 |
| 293 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); | 501 _getSpan(SourceFile file, AstNode node) => file.span(node.offset, node.end); |
| 294 | 502 |
| 295 const MAIN_HEADER = """ | 503 const MAIN_HEADER = """ |
| 296 library app_bootstrap; | 504 library app_bootstrap; |
| 297 | 505 |
| 298 import 'package:polymer/polymer.dart'; | 506 import 'package:polymer/polymer.dart'; |
| 299 import 'package:smoke/static.dart' as smoke; | |
| 300 """; | 507 """; |
| 508 | |
| 509 /// An html visitor that: | |
| 510 /// * finds all polymer expressions and records the getters and setters that | |
| 511 /// will be needed to evaluate them at runtime. | |
| 512 /// * extracts all attributes declared in the `attribute` attributes of | |
| 513 /// polymer elements. | |
| 514 class _HtmlExtractor extends TreeVisitor { | |
| 515 final Map<String, List<String>> publishedAttributes; | |
| 516 final SmokeCodeGenerator generator; | |
| 517 final _SubExpressionVisitor visitor; | |
| 518 bool _inTemplate = false; | |
| 519 | |
| 520 _HtmlExtractor(SmokeCodeGenerator generator, this.publishedAttributes) | |
| 521 : generator = generator, | |
| 522 visitor = new _SubExpressionVisitor(generator); | |
| 523 | |
| 524 void visitElement(Element node) { | |
| 525 if (_inTemplate) _processNormalElement(node); | |
| 526 if (node.localName == 'polymer-element') { | |
| 527 _processPolymerElement(node); | |
| 528 _processNormalElement(node); | |
| 529 } | |
| 530 | |
| 531 if (node.localName == 'template') { | |
| 532 var last = _inTemplate; | |
| 533 _inTemplate = true; | |
| 534 super.visitElement(node); | |
| 535 _inTemplate = last; | |
| 536 } else { | |
| 537 super.visitElement(node); | |
| 538 } | |
| 539 } | |
| 540 | |
| 541 void visitText(Text node) { | |
| 542 if (!_inTemplate) return; | |
| 543 var bindings = _parseMustaches(node.data); | |
| 544 if (bindings == null) return; | |
| 545 for (var e in bindings.expressions) { | |
| 546 _addExpression(e, false, false); | |
| 547 } | |
| 548 } | |
| 549 | |
| 550 /// Regex to split names in the attributes attribute, which supports 'a b c', | |
| 551 /// 'a,b,c', or even 'a b,c'. | |
| 552 static final _ATTRIBUTES_REGEX = new RegExp(r'\s|,'); | |
| 553 | |
| 554 /// Registers getters and setters for all published attributes. | |
| 555 void _processPolymerElement(Element node) { | |
| 556 var tagName = node.attributes['name']; | |
| 557 var value = node.attributes['attributes']; | |
| 558 if (value != null) { | |
| 559 publishedAttributes[tagName] = | |
| 560 value.split(_ATTRIBUTES_REGEX).map((a) => a.trim()).toList(); | |
| 561 } | |
| 562 } | |
| 563 | |
| 564 /// Produces warnings for misuses of on-foo event handlers, and for instanting | |
| 565 /// custom tags incorrectly. | |
| 566 void _processNormalElement(Element node) { | |
| 567 var tag = node.localName; | |
| 568 var isCustomTag = isCustomTagName(tag) || node.attributes['is'] != null; | |
| 569 | |
| 570 // Event handlers only allowed inside polymer-elements | |
| 571 node.attributes.forEach((name, value) { | |
| 572 var bindings = _parseMustaches(value); | |
| 573 if (bindings == null) return; | |
| 574 var isEvent = false; | |
| 575 var isTwoWay = false; | |
| 576 if (name is String) { | |
| 577 name = name.toLowerCase(); | |
| 578 isEvent = name.startsWith('on-'); | |
| 579 isTwoWay = !isEvent && bindings.isWhole && (isCustomTag || | |
|
Jennifer Messerly
2014/03/27 02:20:32
how important is this?
it worries me on the maint
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
We could, not sure. We'd have to measure how much
Jennifer Messerly
2014/03/28 21:48:35
yeah, it would be nice to be in template_binding,
Siggi Cherem (dart-lang)
2014/03/28 22:08:47
Sounds great. I'll file a bug to track as well.
| |
| 580 tag == 'input' && (name == 'value' || name =='checked') || | |
| 581 tag == 'select' && (name == 'selectedindex' || name == 'value') || | |
| 582 tag == 'textarea' && name == 'value'); | |
| 583 } | |
| 584 for (var exp in bindings.expressions) { | |
| 585 _addExpression(exp, isEvent, isTwoWay); | |
| 586 } | |
| 587 }); | |
| 588 } | |
| 589 | |
| 590 void _addExpression(String stringExpression, bool inEvent, bool isTwoWay) { | |
| 591 if (inEvent) { | |
| 592 if (!stringExpression.startsWith("@")) { | |
| 593 generator.addGetter(stringExpression); | |
| 594 generator.addSymbol(stringExpression); | |
| 595 return; | |
| 596 } | |
| 597 stringExpression = stringExpression.substring(1); | |
| 598 } | |
| 599 visitor.run(pe.parse(stringExpression), isTwoWay); | |
| 600 } | |
| 601 } | |
| 602 | |
| 603 /// A polymer-expression visitor that records every getter and setter that will | |
| 604 /// be needed to evaluate a single expression at runtime. | |
| 605 class _SubExpressionVisitor extends pe.RecursiveVisitor { | |
| 606 final SmokeCodeGenerator generator; | |
| 607 bool _includeSetter; | |
| 608 | |
| 609 _SubExpressionVisitor(this.generator); | |
| 610 | |
| 611 /// Visit [exp], and record getters and setters that are needed in order to | |
| 612 /// evaluate it at runtime. [includeSetter] is only true if this expression | |
| 613 /// occured in a context where it could be updated, for example in two-way | |
| 614 /// bindings such as `<input value={{exp}}>`. | |
| 615 void run(pe.Expression exp, bool includeSetter) { | |
| 616 _includeSetter = includeSetter; | |
| 617 visit(exp); | |
| 618 } | |
| 619 | |
| 620 /// Adds a getter and symbol for [name], and optionally a setter. | |
| 621 _add(String name) { | |
| 622 generator.addGetter(name); | |
| 623 generator.addSymbol(name); | |
| 624 if (_includeSetter) generator.addSetter(name); | |
| 625 } | |
| 626 | |
| 627 void preVisitExpression(e) { | |
| 628 // For two-way bindings the outermost expression may be updated, so we need | |
| 629 // both the getter and the setter, but subexpressions only need the getter. | |
| 630 // So we exclude setters as soon as we go deeper in the tree. | |
| 631 _includeSetter = false; | |
| 632 } | |
| 633 | |
| 634 visitIdentifier(pe.Identifier e) { | |
| 635 if (e.value != 'this') _add(e.value); | |
| 636 super.visitIdentifier(e); | |
| 637 } | |
| 638 | |
| 639 visitGetter(pe.Getter e) { | |
| 640 _add(e.name); | |
| 641 super.visitGetter(e); | |
| 642 } | |
| 643 | |
| 644 visitInvoke(pe.Invoke e) { | |
| 645 _includeSetter = false; // Invoke is only valid as an r-value. | |
| 646 _add(e.method); | |
| 647 super.visitInvoke(e); | |
| 648 } | |
| 649 } | |
| 650 | |
| 651 class _Mustaches { | |
| 652 /// Each expression that appears within `{{...}}` and `[[...]]`. | |
| 653 List<String> expressions = []; | |
| 654 | |
| 655 /// Whether the whole text returned by [_parseMustaches] was a single mustache | |
| 656 /// expression. | |
| 657 bool isWhole; | |
| 658 | |
| 659 _Mustaches(this.isWhole); | |
| 660 } | |
| 661 | |
| 662 // TODO(sigmund): this is a simplification of what template-binding does. Could | |
| 663 // we share some code here and in template_binding/src/mustache_tokens.dart? | |
|
Jennifer Messerly
2014/03/27 02:20:32
could you just import that file? it doesn't look l
Siggi Cherem (dart-lang)
2014/03/28 01:04:26
Done. I managed to refactor it slightly and now I'
| |
| 664 _Mustaches _parseMustaches(String text) { | |
| 665 if (text == null || text.isEmpty) return null; | |
| 666 var bindings = null; | |
| 667 var length = text.length; | |
| 668 var lastIndex = 0; | |
| 669 while (lastIndex < length) { | |
| 670 var startIndex = text.indexOf('{{', lastIndex); | |
| 671 var oneTimeStart = text.indexOf('[[', lastIndex); | |
| 672 var oneTime = false; | |
| 673 var terminator = '}}'; | |
| 674 | |
| 675 if (oneTimeStart >= 0 && | |
| 676 (startIndex < 0 || oneTimeStart < startIndex)) { | |
| 677 startIndex = oneTimeStart; | |
| 678 oneTime = true; | |
| 679 terminator = ']]'; | |
| 680 } | |
| 681 | |
| 682 var endIndex = -1; | |
| 683 if (startIndex >= 0) { | |
| 684 endIndex = text.indexOf(terminator, startIndex + 2); | |
| 685 } | |
| 686 | |
| 687 if (endIndex < 0) return bindings; | |
| 688 | |
| 689 if (bindings == null) { | |
| 690 bindings = new _Mustaches(startIndex == 0 && endIndex == length - 2); | |
| 691 } | |
| 692 var pathString = text.substring(startIndex + 2, endIndex).trim(); | |
| 693 bindings.expressions.add(pathString); | |
| 694 lastIndex = endIndex + 2; | |
| 695 } | |
| 696 return bindings; | |
| 697 } | |
| OLD | NEW |