OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 library web_components.build.script_compactor; | |
5 | |
6 import 'dart:async'; | |
7 import 'package:analyzer/analyzer.dart'; | |
8 import 'package:barback/barback.dart'; | |
9 import 'package:code_transformers/assets.dart'; | |
10 import 'package:code_transformers/messages/build_logger.dart'; | |
11 import 'package:html/dom.dart' as dom; | |
12 import 'package:html/parser.dart' as parser; | |
13 import 'package:path/path.dart' as path; | |
14 import 'package:source_maps/refactor.dart' show TextEditTransaction; | |
15 import 'package:source_span/source_span.dart'; | |
16 import 'common.dart'; | |
17 import 'import_crawler.dart'; | |
18 import 'messages.dart'; | |
19 | |
20 /// Transformer which combines all dart scripts found in html imports into one | |
21 /// new bootstrap file, and replaces the old entry point script with that file. | |
22 /// | |
23 /// Note: Does not delete the original script files (it can't because the | |
24 /// imports may live in other packages). The [ImportInlinerTransformer] will not | |
25 /// copy scripts when inlining imports into your entry point to compensate for | |
26 /// this. | |
27 class ScriptCompactorTransformer extends Transformer { | |
28 final List<String> entryPoints; | |
29 | |
30 ScriptCompactorTransformer([this.entryPoints]); | |
31 | |
32 bool isPrimary(AssetId id) { | |
33 if (entryPoints != null) return entryPoints.contains(id.path); | |
34 // If no entry point is supplied, then any html file under web/ or test/ is | |
35 // an entry point. | |
36 return (id.path.startsWith('web/') || id.path.startsWith('test/')) && | |
37 id.path.endsWith('.html'); | |
38 } | |
39 | |
40 apply(Transform transform) { | |
41 var logger = new BuildLogger(transform); | |
42 return new ScriptCompactor(transform, transform.primaryInput.id, logger) | |
43 .run() | |
44 .then((Asset bootstrap) { | |
45 if (bootstrap == null) return null; | |
46 return transform.primaryInput.readAsString().then((html) { | |
47 var doc = parser.parse(html); | |
48 var mainScriptTag = doc.querySelector('script[type="$dartType"]'); | |
49 mainScriptTag.attributes['src'] = | |
50 _importPath(bootstrap.id, transform.primaryInput.id); | |
51 mainScriptTag.text = ''; | |
52 | |
53 transform.addOutput( | |
54 new Asset.fromString(transform.primaryInput.id, doc.outerHtml)); | |
55 }); | |
56 }); | |
57 } | |
58 } | |
59 | |
60 /// Helper class which does all the script compacting for a single entry point. | |
61 class ScriptCompactor { | |
62 /// Can be an AggregateTransform or Transform | |
63 final transform; | |
64 | |
65 /// The primary input to start from. | |
66 final AssetId primaryInput; | |
67 | |
68 /// The logger to use. | |
69 final BuildLogger logger; | |
70 | |
71 /// How many inline scripts were extracted. | |
72 int inlineScriptCounter = 0; | |
73 | |
74 /// Id representing the dart script which lives in the primaryInput. | |
75 AssetId mainScript; | |
76 | |
77 /// Ids of all the scripts found in html imports. | |
78 final Set<AssetId> importScripts = new Set<AssetId>(); | |
79 | |
80 ScriptCompactor(this.transform, this.primaryInput, this.logger); | |
81 | |
82 Future<Asset> run() { | |
83 var crawler = new ImportCrawler(transform, primaryInput, logger); | |
84 return crawler.crawlImports().then((imports) { | |
85 Future extractScripts(id) => | |
86 _extractInlineScripts(id, imports[id].document); | |
87 | |
88 return Future.forEach(imports.keys, extractScripts).then((_) { | |
89 if (mainScript == null) { | |
90 logger.error( | |
91 exactlyOneScriptPerEntryPoint.create({'url': primaryInput.path})); | |
92 return null; | |
93 } | |
94 | |
95 var primaryDocument = imports[primaryInput].document; | |
96 assert(primaryDocument != null); | |
97 | |
98 // Create the new bootstrap file and return its AssetId. | |
99 return _buildBootstrapFile(mainScript, importScripts); | |
100 }); | |
101 }); | |
102 } | |
103 | |
104 /// Builds the bootstrap file and returns the path to it relative to | |
105 /// [primaryInput]. | |
106 Asset _buildBootstrapFile(AssetId mainScript, Set<AssetId> importScripts) { | |
107 var bootstrapId = new AssetId(primaryInput.package, | |
108 primaryInput.path.replaceFirst('.html', '.bootstrap.dart')); | |
109 | |
110 var buffer = new StringBuffer(); | |
111 buffer.writeln('library ${_libraryNameFor(bootstrapId)};'); | |
112 buffer.writeln(); | |
113 var i = 0; | |
114 for (var script in importScripts) { | |
115 var path = _importPath(script, primaryInput); | |
116 buffer.writeln("import '$path' as i$i;"); | |
117 i++; | |
118 } | |
119 var mainScriptPath = _importPath(mainScript, primaryInput); | |
120 buffer.writeln("import '$mainScriptPath' as i$i;"); | |
121 buffer.writeln(); | |
122 buffer.writeln('main() => i$i.main();'); | |
123 | |
124 var bootstrap = new Asset.fromString(bootstrapId, '$buffer'); | |
125 transform.addOutput(bootstrap); | |
126 return bootstrap; | |
127 } | |
128 | |
129 /// Split inline scripts into their own files. We need to do this for dart2js | |
130 /// to be able to compile them. | |
131 /// | |
132 /// This also validates that there weren't any duplicate scripts. | |
133 Future _extractInlineScripts(AssetId asset, dom.Document doc) { | |
134 var scripts = doc.querySelectorAll('script[type="$dartType"]'); | |
135 return Future.forEach(scripts, (script) { | |
136 var type = script.attributes['type']; | |
137 var src = script.attributes['src']; | |
138 | |
139 if (src != null) { | |
140 return _addScript( | |
141 asset, uriToAssetId(asset, src, logger, script.sourceSpan), | |
142 span: script.sourceSpan); | |
143 } | |
144 | |
145 final count = inlineScriptCounter++; | |
146 var code = script.text; | |
147 // TODO(sigmund): ensure this path is unique (dartbug.com/12618). | |
148 var newId = primaryInput.addExtension('.$count.dart'); | |
149 if (!_hasLibraryDirective(code)) { | |
150 var libName = _libraryNameFor(primaryInput, count); | |
151 code = "library $libName;\n$code"; | |
152 } | |
153 | |
154 // Normalize dart import paths. | |
155 code = _normalizeDartImports(code, asset, primaryInput); | |
156 | |
157 // Write out the file and record it. | |
158 transform.addOutput(new Asset.fromString(newId, code)); | |
159 | |
160 return _addScript(asset, newId, validate: false).then((_) { | |
161 // If in the entry point, replace the inline script with one pointing to | |
162 // the new source file. | |
163 if (primaryInput == asset) { | |
164 script.text = ''; | |
165 script.attributes['src'] = path.url.relative(newId.path, | |
166 from: path.url.dirname(primaryInput.path)); | |
167 } | |
168 }); | |
169 }); | |
170 } | |
171 | |
172 // Normalize dart import paths when moving code from one asset to another. | |
173 String _normalizeDartImports(String code, AssetId from, AssetId to) { | |
174 var unit = parseDirectives(code, suppressErrors: true); | |
175 var file = new SourceFile(code, url: spanUrlFor(from, to, logger)); | |
176 var output = new TextEditTransaction(code, file); | |
177 var foundLibraryDirective = false; | |
178 for (Directive directive in unit.directives) { | |
179 if (directive is UriBasedDirective) { | |
180 var uri = directive.uri.stringValue; | |
181 var span = getSpan(file, directive.uri); | |
182 | |
183 var id = uriToAssetId(from, uri, logger, span, errorOnAbsolute: false); | |
184 if (id == null) continue; | |
185 | |
186 var primaryId = primaryInput; | |
187 var newUri = assetUrlFor(id, primaryId, logger); | |
188 if (newUri != uri) { | |
189 output.edit(span.start.offset, span.end.offset, "'$newUri'"); | |
190 } | |
191 } else if (directive is LibraryDirective) { | |
192 foundLibraryDirective = true; | |
193 } | |
194 } | |
195 | |
196 if (!output.hasEdits) return code; | |
197 | |
198 // TODO(sigmund): emit source maps when barback supports it (see | |
199 // dartbug.com/12340) | |
200 return (output.commit()..build(file.url.toString())).text; | |
201 } | |
202 | |
203 Future _addScript(AssetId from, AssetId scriptId, | |
204 {bool validate: true, SourceSpan span}) { | |
205 var validateFuture; | |
206 if (validate && !importScripts.contains(scriptId)) { | |
207 validateFuture = transform.hasInput(scriptId); | |
208 } else { | |
209 validateFuture = new Future.value(true); | |
210 } | |
211 return validateFuture.then((exists) { | |
212 if (!exists) { | |
213 logger.warning(scriptFileNotFound.create({'url': scriptId}), | |
214 span: span); | |
215 } | |
216 | |
217 if (from == primaryInput) { | |
218 if (mainScript != null) { | |
219 logger | |
220 .error(exactlyOneScriptPerEntryPoint.create({'url': from.path})); | |
221 } | |
222 mainScript = scriptId; | |
223 } else { | |
224 importScripts.add(scriptId); | |
225 } | |
226 }); | |
227 } | |
228 } | |
229 | |
230 /// Generate a library name for an asset. | |
231 String _libraryNameFor(AssetId id, [int suffix]) { | |
232 var name = '${path.withoutExtension(id.path)}_' | |
233 '${path.extension(id.path).substring(1)}'; | |
234 if (name.startsWith('lib/')) name = name.substring(4); | |
235 name = name.split('/').map((part) { | |
236 part = part.replaceAll(_invalidLibCharsRegex, '_'); | |
237 if (part.startsWith(_numRegex)) part = '_${part}'; | |
238 return part; | |
239 }).join("."); | |
240 var suffixString = suffix != null ? '_$suffix' : ''; | |
241 return '${id.package}.${name}$suffixString'; | |
242 } | |
243 | |
244 /// Parse [code] and determine whether it has a library directive. | |
245 bool _hasLibraryDirective(String code) => | |
246 parseDirectives(code, suppressErrors: true).directives | |
247 .any((d) => d is LibraryDirective); | |
248 | |
249 /// Returns the dart import path to reach [id] relative to [primaryInput]. | |
250 String _importPath(AssetId id, AssetId primaryInput) { | |
251 var parts = path.url.split(id.path); | |
252 if (parts[0] == 'lib') { | |
253 parts[0] = id.package; | |
254 return 'package:${path.url.joinAll(parts)}'; | |
255 } | |
256 return path.url.relative(id.path, from: path.url.dirname(primaryInput.path)); | |
257 } | |
258 | |
259 // Constant and final variables | |
260 final _invalidLibCharsRegex = new RegExp('[^a-z0-9_]'); | |
261 final _numRegex = new RegExp('[0-9]'); | |
OLD | NEW |