| 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 |