OLD | NEW |
1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2015, 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 library initialize.transformer; | 4 library initialize.transformer; |
5 | 5 |
6 import 'dart:async'; | 6 import 'dart:async'; |
7 import 'dart:collection' show Queue; | 7 import 'dart:collection' show Queue; |
8 import 'package:analyzer/src/generated/ast.dart'; | 8 import 'package:analyzer/src/generated/ast.dart'; |
9 import 'package:analyzer/src/generated/element.dart'; | 9 import 'package:analyzer/src/generated/element.dart'; |
10 import 'package:barback/barback.dart'; | 10 import 'package:barback/barback.dart'; |
| 11 import 'package:code_transformers/assets.dart'; |
11 import 'package:code_transformers/resolver.dart'; | 12 import 'package:code_transformers/resolver.dart'; |
| 13 import 'package:glob/glob.dart'; |
12 import 'package:html5lib/dom.dart' as dom; | 14 import 'package:html5lib/dom.dart' as dom; |
13 import 'package:html5lib/parser.dart' show parse; | 15 import 'package:html5lib/parser.dart' show parse; |
14 import 'package:path/path.dart' as path; | 16 import 'package:path/path.dart' as path; |
15 | 17 |
16 /// Removes the mirror-based initialization logic and replaces it with static | 18 /// Removes the mirror-based initialization logic and replaces it with static |
17 /// logic. | 19 /// logic. |
18 class InitializeTransformer extends Transformer { | 20 class InitializeTransformer extends Transformer { |
19 final Resolvers _resolvers; | 21 final Resolvers _resolvers; |
20 final String _entryPoint; | 22 final Iterable<Glob> _entryPointGlobs; |
21 final String _newEntryPoint; | 23 final bool _errorIfNotFound; |
22 final String _htmlEntryPoint; | |
23 | 24 |
24 InitializeTransformer( | 25 InitializeTransformer(List<String> entryPoints, {bool errorIfNotFound: true}) |
25 this._entryPoint, this._newEntryPoint, this._htmlEntryPoint) | 26 : _entryPointGlobs = entryPoints.map((e) => new Glob(e)), |
26 : _resolvers = new Resolvers.fromMock({ | 27 _errorIfNotFound = errorIfNotFound, |
27 // The list of types below is derived from: | 28 _resolvers = new Resolvers.fromMock({ |
28 // * types that are used internally by the resolver (see | 29 // The list of types below is derived from: |
29 // _initializeFrom in resolver.dart). | 30 // * types that are used internally by the resolver (see |
30 // TODO(jakemac): Move this into code_transformers so it can be shared. | 31 // _initializeFrom in resolver.dart). |
31 'dart:core': ''' | 32 // TODO(jakemac): Move this into code_transformers so it can be shared
. |
| 33 'dart:core': ''' |
32 library dart.core; | 34 library dart.core; |
33 class Object {} | 35 class Object {} |
34 class Function {} | 36 class Function {} |
35 class StackTrace {} | 37 class StackTrace {} |
36 class Symbol {} | 38 class Symbol {} |
37 class Type {} | 39 class Type {} |
38 | 40 |
39 class String extends Object {} | 41 class String extends Object {} |
40 class bool extends Object {} | 42 class bool extends Object {} |
41 class num extends Object {} | 43 class num extends Object {} |
42 class int extends num {} | 44 class int extends num {} |
43 class double extends num {} | 45 class double extends num {} |
44 class DateTime extends Object {} | 46 class DateTime extends Object {} |
45 class Null extends Object {} | 47 class Null extends Object {} |
46 | 48 |
47 class Deprecated extends Object { | 49 class Deprecated extends Object { |
48 final String expires; | 50 final String expires; |
49 const Deprecated(this.expires); | 51 const Deprecated(this.expires); |
50 } | 52 } |
51 const Object deprecated = const Deprecated("next release"); | 53 const Object deprecated = const Deprecated("next release"); |
52 class _Override { const _Override(); } | 54 class _Override { const _Override(); } |
53 const Object override = const _Override(); | 55 const Object override = const _Override(); |
54 class _Proxy { const _Proxy(); } | 56 class _Proxy { const _Proxy(); } |
55 const Object proxy = const _Proxy(); | 57 const Object proxy = const _Proxy(); |
56 | 58 |
57 class List<V> extends Object {} | 59 class List<V> extends Object {} |
58 class Map<K, V> extends Object {} | 60 class Map<K, V> extends Object {} |
59 ''', | 61 ''', |
60 'dart:html': ''' | 62 'dart:html': ''' |
61 library dart.html; | 63 library dart.html; |
62 class HtmlElement {} | 64 class HtmlElement {} |
63 ''', | 65 ''', |
| 66 }); |
| 67 |
| 68 factory InitializeTransformer.asPlugin(BarbackSettings settings) => |
| 69 new InitializeTransformer(_readFileList(settings, 'entry_points')); |
| 70 |
| 71 bool isPrimary(AssetId id) => _entryPointGlobs.any((g) => g.matches(id.path)); |
| 72 |
| 73 Future apply(Transform transform) { |
| 74 if (transform.primaryInput.id.path.endsWith('.dart')) { |
| 75 return _buildBootstrapFile(transform); |
| 76 } else if (transform.primaryInput.id.path.endsWith('.html')) { |
| 77 return transform.primaryInput.readAsString().then((html) { |
| 78 var document = parse(html); |
| 79 var originalDartFile = |
| 80 _findMainScript(document, transform.primaryInput.id, transform); |
| 81 return _buildBootstrapFile(transform, primaryId: originalDartFile).then( |
| 82 (AssetId newDartFile) { |
| 83 return _replaceEntryWithBootstrap(transform, document, |
| 84 transform.primaryInput.id, originalDartFile, newDartFile); |
| 85 }); |
64 }); | 86 }); |
65 | 87 } else { |
66 factory InitializeTransformer.asPlugin(BarbackSettings settings) { | 88 transform.logger.warning( |
67 var entryPoint = settings.configuration['entry_point']; | 89 'Invalid entry point ${transform.primaryInput.id}. Must be either a ' |
68 var newEntryPoint = settings.configuration['new_entry_point']; | 90 '.dart or .html file.'); |
69 if (newEntryPoint == null) { | |
70 newEntryPoint = entryPoint.replaceFirst('.dart', '.bootstrap.dart'); | |
71 } | 91 } |
72 var htmlEntryPoint = settings.configuration['html_entry_point']; | 92 return new Future.value(); |
73 return new InitializeTransformer(entryPoint, newEntryPoint, htmlEntryPoint); | |
74 } | 93 } |
75 | 94 |
76 bool isPrimary(AssetId id) => | 95 // Returns the AssetId of the newly created bootstrap file. |
77 _entryPoint == id.path || _htmlEntryPoint == id.path; | 96 Future<AssetId> _buildBootstrapFile(Transform transform, |
78 | 97 {AssetId primaryId}) { |
79 Future apply(Transform transform) { | 98 if (primaryId == null) primaryId = transform.primaryInput.id; |
80 if (transform.primaryInput.id.path == _entryPoint) { | 99 var newEntryPointId = new AssetId(primaryId.package, |
81 return _buildBootstrapFile(transform); | 100 '${path.url.withoutExtension(primaryId.path)}.initialize.dart'); |
82 } else if (transform.primaryInput.id.path == _htmlEntryPoint) { | |
83 return _replaceEntryWithBootstrap(transform); | |
84 } | |
85 return null; | |
86 } | |
87 | |
88 Future _buildBootstrapFile(Transform transform) { | |
89 var newEntryPointId = | |
90 new AssetId(transform.primaryInput.id.package, _newEntryPoint); | |
91 return transform.hasInput(newEntryPointId).then((exists) { | 101 return transform.hasInput(newEntryPointId).then((exists) { |
92 if (exists) { | 102 if (exists) { |
93 transform.logger | 103 transform.logger |
94 .error('New entry point file $newEntryPointId already exists.'); | 104 .error('New entry point file $newEntryPointId already exists.'); |
95 } else { | 105 return null; |
96 return _resolvers.get(transform).then((resolver) { | |
97 new _BootstrapFileBuilder(resolver, transform, | |
98 transform.primaryInput.id, newEntryPointId).run(); | |
99 resolver.release(); | |
100 }); | |
101 } | 106 } |
| 107 |
| 108 return _resolvers.get(transform, [primaryId]).then((resolver) { |
| 109 new _BootstrapFileBuilder(resolver, transform, primaryId, |
| 110 newEntryPointId, _errorIfNotFound).run(); |
| 111 resolver.release(); |
| 112 return newEntryPointId; |
| 113 }); |
102 }); | 114 }); |
103 } | 115 } |
104 | 116 |
105 Future _replaceEntryWithBootstrap(Transform transform) { | 117 // Replaces script tags pointing to [originalDartFile] with [newDartFile] in |
106 // For now at least, _htmlEntryPoint, _entryPoint, and _newEntryPoint need | 118 // [entryPoint]. |
107 // to be in the same folder. | 119 void _replaceEntryWithBootstrap(Transform transform, dom.Document document, |
108 // TODO(jakemac): support package urls with _entryPoint or _newEntryPoint | 120 AssetId entryPoint, AssetId originalDartFile, AssetId newDartFile) { |
109 // in `lib`, and _htmlEntryPoint in another directory. | 121 var found = false; |
110 var _expectedDir = path.split(_htmlEntryPoint)[0]; | 122 var scripts = document |
111 if (_expectedDir != path.split(_entryPoint)[0] || | 123 .querySelectorAll('script[type="application/dart"]') |
112 _expectedDir != path.split(_newEntryPoint)[0]) { | 124 .where((script) => uriToAssetId(entryPoint, script.attributes['src'], |
| 125 transform.logger, script.sourceSpan) == originalDartFile) |
| 126 .toList(); |
| 127 |
| 128 if (scripts.length != 1) { |
113 transform.logger.error( | 129 transform.logger.error( |
114 'htmlEntryPoint, entryPoint, and newEntryPoint(if supplied) all must ' | 130 'Expected exactly one script pointing to $originalDartFile in ' |
115 'be in the same top level directory.'); | 131 '$entryPoint, but found ${scripts.length}.'); |
| 132 return; |
| 133 } |
| 134 scripts[0].attributes['src'] = path.url.relative(newDartFile.path, |
| 135 from: path.dirname(entryPoint.path)); |
| 136 transform.addOutput(new Asset.fromString(entryPoint, document.outerHtml)); |
| 137 } |
| 138 |
| 139 AssetId _findMainScript( |
| 140 dom.Document document, AssetId entryPoint, Transform transform) { |
| 141 var scripts = document.querySelectorAll('script[type="application/dart"]'); |
| 142 if (scripts.length != 1) { |
| 143 transform.logger.error('Expected exactly one dart script in $entryPoint ' |
| 144 'but found ${scripts.length}.'); |
| 145 return null; |
116 } | 146 } |
117 | 147 |
118 return transform.primaryInput.readAsString().then((String html) { | 148 var src = scripts[0].attributes['src']; |
119 var found = false; | 149 // TODO(jakemac): Support inline scripts, |
120 var doc = parse(html); | 150 // https://github.com/dart-lang/initialize/issues/20 |
121 var scripts = doc.querySelectorAll('script[type="application/dart"]'); | 151 if (src == null) { |
122 for (dom.Element script in scripts) { | 152 transform.logger.error('Inline scripts are not supported at this time.'); |
123 if (!_isEntryPointScript(script)) continue; | 153 return null; |
124 script.attributes['src'] = _relativeDartEntryPath(_newEntryPoint); | 154 } |
125 found = true; | 155 |
126 } | 156 return uriToAssetId( |
127 if (!found) { | 157 entryPoint, src, transform.logger, scripts[0].sourceSpan); |
128 transform.logger.error( | |
129 'Unable to find script for $_entryPoint in $_htmlEntryPoint.'); | |
130 } | |
131 return transform.addOutput( | |
132 new Asset.fromString(transform.primaryInput.id, doc.outerHtml)); | |
133 }); | |
134 } | 158 } |
135 | |
136 // Checks if the src of this script tag is pointing at `_entryPoint`. | |
137 bool _isEntryPointScript(dom.Element script) => | |
138 path.normalize(script.attributes['src']) == | |
139 _relativeDartEntryPath(_entryPoint); | |
140 | |
141 // The relative path from `_htmlEntryPoint` to `dartEntry`. You must ensure | |
142 // that neither of these is null before calling this function. | |
143 String _relativeDartEntryPath(String dartEntry) => | |
144 path.relative(dartEntry, from: path.dirname(_htmlEntryPoint)); | |
145 } | 159 } |
146 | 160 |
147 class _BootstrapFileBuilder { | 161 class _BootstrapFileBuilder { |
148 final Resolver _resolver; | 162 final Resolver _resolver; |
149 final Transform _transform; | 163 final Transform _transform; |
| 164 final bool _errorIfNotFound; |
150 AssetId _entryPoint; | 165 AssetId _entryPoint; |
151 AssetId _newEntryPoint; | 166 AssetId _newEntryPoint; |
152 | 167 |
153 /// The resolved initialize library. | 168 /// The resolved initialize library. |
154 LibraryElement _initializeLibrary; | 169 LibraryElement _initializeLibrary; |
155 /// The resolved Initializer class from the initialize library. | 170 /// The resolved Initializer class from the initialize library. |
156 ClassElement _initializer; | 171 ClassElement _initializer; |
157 | 172 |
158 /// Queue for intialization annotations. | 173 /// Queue for intialization annotations. |
159 final _initQueue = new Queue<_InitializerData>(); | 174 final _initQueue = new Queue<_InitializerData>(); |
160 /// All the annotations we have seen for each element | 175 /// All the annotations we have seen for each element |
161 final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>(); | 176 final _seenAnnotations = new Map<Element, Set<ElementAnnotation>>(); |
162 | 177 |
163 TransformLogger _logger; | 178 TransformLogger _logger; |
164 | 179 |
165 _BootstrapFileBuilder( | 180 _BootstrapFileBuilder(this._resolver, this._transform, this._entryPoint, |
166 this._resolver, this._transform, this._entryPoint, this._newEntryPoint) { | 181 this._newEntryPoint, this._errorIfNotFound) { |
167 _logger = _transform.logger; | 182 _logger = _transform.logger; |
168 _initializeLibrary = | 183 _initializeLibrary = |
169 _resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart')); | 184 _resolver.getLibrary(new AssetId('initialize', 'lib/initialize.dart')); |
170 _initializer = _initializeLibrary.getType('Initializer'); | 185 if (_initializeLibrary != null) { |
| 186 _initializer = _initializeLibrary.getType('Initializer'); |
| 187 } else if (_errorIfNotFound) { |
| 188 _logger.warning('Unable to read "package:initialize/initialize.dart". ' |
| 189 'This file must be imported via $_entryPoint or a transitive ' |
| 190 'dependency.'); |
| 191 } |
171 } | 192 } |
172 | 193 |
173 /// Adds the new entry point file to the transform. Should only be ran once. | 194 /// Adds the new entry point file to the transform. Should only be ran once. |
174 void run() { | 195 void run() { |
175 var entryLib = _resolver.getLibrary(_entryPoint); | 196 var entryLib = _resolver.getLibrary(_entryPoint); |
176 _readLibraries(entryLib); | 197 _readLibraries(entryLib); |
177 | 198 |
178 _transform.addOutput( | 199 _transform.addOutput( |
179 new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib))); | 200 new Asset.fromString(_newEntryPoint, _buildNewEntryPoint(entryLib))); |
180 } | 201 } |
(...skipping 264 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
445 _logger.error('Unsupported argument to initializer constructor.'); | 466 _logger.error('Unsupported argument to initializer constructor.'); |
446 } | 467 } |
447 } else { | 468 } else { |
448 _logger.error('Only literals and identifiers are allowed for initializer ' | 469 _logger.error('Only literals and identifiers are allowed for initializer ' |
449 'expressions, found $expression.'); | 470 'expressions, found $expression.'); |
450 } | 471 } |
451 return buffer.toString(); | 472 return buffer.toString(); |
452 } | 473 } |
453 | 474 |
454 bool _isInitializer(InterfaceType type) { | 475 bool _isInitializer(InterfaceType type) { |
| 476 // If `_initializer` wasn't found then it was never loaded (even |
| 477 // transitively), and so no annotations can be initializers. |
| 478 if (_initializer == null) return false; |
455 if (type == null) return false; | 479 if (type == null) return false; |
456 if (type.element.type == _initializer.type) return true; | 480 if (type.element.type == _initializer.type) return true; |
457 if (_isInitializer(type.superclass)) return true; | 481 if (_isInitializer(type.superclass)) return true; |
458 for (var interface in type.interfaces) { | 482 for (var interface in type.interfaces) { |
459 if (_isInitializer(interface)) return true; | 483 if (_isInitializer(interface)) return true; |
460 } | 484 } |
461 return false; | 485 return false; |
462 } | 486 } |
463 | 487 |
464 /// Retrieves all top-level methods that are visible if you were to import | 488 /// Retrieves all top-level methods that are visible if you were to import |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
537 })).map((import) => import.importedLibrary); | 561 })).map((import) => import.importedLibrary); |
538 } | 562 } |
539 | 563 |
540 // Element/ElementAnnotation pair. | 564 // Element/ElementAnnotation pair. |
541 class _InitializerData { | 565 class _InitializerData { |
542 final Element element; | 566 final Element element; |
543 final ElementAnnotation annotation; | 567 final ElementAnnotation annotation; |
544 | 568 |
545 _InitializerData(this.element, this.annotation); | 569 _InitializerData(this.element, this.annotation); |
546 } | 570 } |
| 571 |
| 572 // Reads a file list from a barback settings configuration field. |
| 573 _readFileList(BarbackSettings settings, String field) { |
| 574 var value = settings.configuration[field]; |
| 575 if (value == null) return null; |
| 576 var files = []; |
| 577 bool error; |
| 578 if (value is List) { |
| 579 files = value; |
| 580 error = value.any((e) => e is! String); |
| 581 } else if (value is String) { |
| 582 files = [value]; |
| 583 error = false; |
| 584 } else { |
| 585 error = true; |
| 586 } |
| 587 if (error) { |
| 588 print('Bad value for "$field" in the initialize transformer. ' |
| 589 'Expected either one String or a list of Strings.'); |
| 590 } |
| 591 return files; |
| 592 } |
OLD | NEW |