1/* -*- Mode: js; js-indent-level: 2; -*- */ 2/* 3 * Copyright 2011 Mozilla Foundation and contributors 4 * Licensed under the New BSD license. See LICENSE or: 5 * http://opensource.org/licenses/BSD-3-Clause 6 */ 7 8var base64VLQ = require('./base64-vlq'); 9var util = require('./util'); 10var ArraySet = require('./array-set').ArraySet; 11var MappingList = require('./mapping-list').MappingList; 12 13/** 14 * An instance of the SourceMapGenerator represents a source map which is 15 * being built incrementally. You may pass an object with the following 16 * properties: 17 * 18 * - file: The filename of the generated source. 19 * - sourceRoot: A root for all relative URLs in this source map. 20 */ 21function SourceMapGenerator(aArgs) { 22 if (!aArgs) { 23 aArgs = {}; 24 } 25 this._file = util.getArg(aArgs, 'file', null); 26 this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null); 27 this._skipValidation = util.getArg(aArgs, 'skipValidation', false); 28 this._sources = new ArraySet(); 29 this._names = new ArraySet(); 30 this._mappings = new MappingList(); 31 this._sourcesContents = null; 32} 33 34SourceMapGenerator.prototype._version = 3; 35 36/** 37 * Creates a new SourceMapGenerator based on a SourceMapConsumer 38 * 39 * @param aSourceMapConsumer The SourceMap. 40 */ 41SourceMapGenerator.fromSourceMap = 42 function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) { 43 var sourceRoot = aSourceMapConsumer.sourceRoot; 44 var generator = new SourceMapGenerator({ 45 file: aSourceMapConsumer.file, 46 sourceRoot: sourceRoot 47 }); 48 aSourceMapConsumer.eachMapping(function (mapping) { 49 var newMapping = { 50 generated: { 51 line: mapping.generatedLine, 52 column: mapping.generatedColumn 53 } 54 }; 55 56 if (mapping.source != null) { 57 newMapping.source = mapping.source; 58 if (sourceRoot != null) { 59 newMapping.source = util.relative(sourceRoot, newMapping.source); 60 } 61 62 newMapping.original = { 63 line: mapping.originalLine, 64 column: mapping.originalColumn 65 }; 66 67 if (mapping.name != null) { 68 newMapping.name = mapping.name; 69 } 70 } 71 72 generator.addMapping(newMapping); 73 }); 74 aSourceMapConsumer.sources.forEach(function (sourceFile) { 75 var sourceRelative = sourceFile; 76 if (sourceRoot !== null) { 77 sourceRelative = util.relative(sourceRoot, sourceFile); 78 } 79 80 if (!generator._sources.has(sourceRelative)) { 81 generator._sources.add(sourceRelative); 82 } 83 84 var content = aSourceMapConsumer.sourceContentFor(sourceFile); 85 if (content != null) { 86 generator.setSourceContent(sourceFile, content); 87 } 88 }); 89 return generator; 90 }; 91 92/** 93 * Add a single mapping from original source line and column to the generated 94 * source's line and column for this source map being created. The mapping 95 * object should have the following properties: 96 * 97 * - generated: An object with the generated line and column positions. 98 * - original: An object with the original line and column positions. 99 * - source: The original source file (relative to the sourceRoot). 100 * - name: An optional original token name for this mapping. 101 */ 102SourceMapGenerator.prototype.addMapping = 103 function SourceMapGenerator_addMapping(aArgs) { 104 var generated = util.getArg(aArgs, 'generated'); 105 var original = util.getArg(aArgs, 'original', null); 106 var source = util.getArg(aArgs, 'source', null); 107 var name = util.getArg(aArgs, 'name', null); 108 109 if (!this._skipValidation) { 110 this._validateMapping(generated, original, source, name); 111 } 112 113 if (source != null) { 114 source = String(source); 115 if (!this._sources.has(source)) { 116 this._sources.add(source); 117 } 118 } 119 120 if (name != null) { 121 name = String(name); 122 if (!this._names.has(name)) { 123 this._names.add(name); 124 } 125 } 126 127 this._mappings.add({ 128 generatedLine: generated.line, 129 generatedColumn: generated.column, 130 originalLine: original != null && original.line, 131 originalColumn: original != null && original.column, 132 source: source, 133 name: name 134 }); 135 }; 136 137/** 138 * Set the source content for a source file. 139 */ 140SourceMapGenerator.prototype.setSourceContent = 141 function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { 142 var source = aSourceFile; 143 if (this._sourceRoot != null) { 144 source = util.relative(this._sourceRoot, source); 145 } 146 147 if (aSourceContent != null) { 148 // Add the source content to the _sourcesContents map. 149 // Create a new _sourcesContents map if the property is null. 150 if (!this._sourcesContents) { 151 this._sourcesContents = Object.create(null); 152 } 153 this._sourcesContents[util.toSetString(source)] = aSourceContent; 154 } else if (this._sourcesContents) { 155 // Remove the source file from the _sourcesContents map. 156 // If the _sourcesContents map is empty, set the property to null. 157 delete this._sourcesContents[util.toSetString(source)]; 158 if (Object.keys(this._sourcesContents).length === 0) { 159 this._sourcesContents = null; 160 } 161 } 162 }; 163 164/** 165 * Applies the mappings of a sub-source-map for a specific source file to the 166 * source map being generated. Each mapping to the supplied source file is 167 * rewritten using the supplied source map. Note: The resolution for the 168 * resulting mappings is the minimium of this map and the supplied map. 169 * 170 * @param aSourceMapConsumer The source map to be applied. 171 * @param aSourceFile Optional. The filename of the source file. 172 * If omitted, SourceMapConsumer's file property will be used. 173 * @param aSourceMapPath Optional. The dirname of the path to the source map 174 * to be applied. If relative, it is relative to the SourceMapConsumer. 175 * This parameter is needed when the two source maps aren't in the same 176 * directory, and the source map to be applied contains relative source 177 * paths. If so, those relative source paths need to be rewritten 178 * relative to the SourceMapGenerator. 179 */ 180SourceMapGenerator.prototype.applySourceMap = 181 function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { 182 var sourceFile = aSourceFile; 183 // If aSourceFile is omitted, we will use the file property of the SourceMap 184 if (aSourceFile == null) { 185 if (aSourceMapConsumer.file == null) { 186 throw new Error( 187 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' + 188 'or the source map\'s "file" property. Both were omitted.' 189 ); 190 } 191 sourceFile = aSourceMapConsumer.file; 192 } 193 var sourceRoot = this._sourceRoot; 194 // Make "sourceFile" relative if an absolute Url is passed. 195 if (sourceRoot != null) { 196 sourceFile = util.relative(sourceRoot, sourceFile); 197 } 198 // Applying the SourceMap can add and remove items from the sources and 199 // the names array. 200 var newSources = new ArraySet(); 201 var newNames = new ArraySet(); 202 203 // Find mappings for the "sourceFile" 204 this._mappings.unsortedForEach(function (mapping) { 205 if (mapping.source === sourceFile && mapping.originalLine != null) { 206 // Check if it can be mapped by the source map, then update the mapping. 207 var original = aSourceMapConsumer.originalPositionFor({ 208 line: mapping.originalLine, 209 column: mapping.originalColumn 210 }); 211 if (original.source != null) { 212 // Copy mapping 213 mapping.source = original.source; 214 if (aSourceMapPath != null) { 215 mapping.source = util.join(aSourceMapPath, mapping.source) 216 } 217 if (sourceRoot != null) { 218 mapping.source = util.relative(sourceRoot, mapping.source); 219 } 220 mapping.originalLine = original.line; 221 mapping.originalColumn = original.column; 222 if (original.name != null) { 223 mapping.name = original.name; 224 } 225 } 226 } 227 228 var source = mapping.source; 229 if (source != null && !newSources.has(source)) { 230 newSources.add(source); 231 } 232 233 var name = mapping.name; 234 if (name != null && !newNames.has(name)) { 235 newNames.add(name); 236 } 237 238 }, this); 239 this._sources = newSources; 240 this._names = newNames; 241 242 // Copy sourcesContents of applied map. 243 aSourceMapConsumer.sources.forEach(function (sourceFile) { 244 var content = aSourceMapConsumer.sourceContentFor(sourceFile); 245 if (content != null) { 246 if (aSourceMapPath != null) { 247 sourceFile = util.join(aSourceMapPath, sourceFile); 248 } 249 if (sourceRoot != null) { 250 sourceFile = util.relative(sourceRoot, sourceFile); 251 } 252 this.setSourceContent(sourceFile, content); 253 } 254 }, this); 255 }; 256 257/** 258 * A mapping can have one of the three levels of data: 259 * 260 * 1. Just the generated position. 261 * 2. The Generated position, original position, and original source. 262 * 3. Generated and original position, original source, as well as a name 263 * token. 264 * 265 * To maintain consistency, we validate that any new mapping being added falls 266 * in to one of these categories. 267 */ 268SourceMapGenerator.prototype._validateMapping = 269 function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, 270 aName) { 271 // When aOriginal is truthy but has empty values for .line and .column, 272 // it is most likely a programmer error. In this case we throw a very 273 // specific error message to try to guide them the right way. 274 // For example: https://github.com/Polymer/polymer-bundler/pull/519 275 if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') { 276 throw new Error( 277 'original.line and original.column are not numbers -- you probably meant to omit ' + 278 'the original mapping entirely and only map the generated position. If so, pass ' + 279 'null for the original mapping instead of an object with empty or null values.' 280 ); 281 } 282 283 if (aGenerated && 'line' in aGenerated && 'column' in aGenerated 284 && aGenerated.line > 0 && aGenerated.column >= 0 285 && !aOriginal && !aSource && !aName) { 286 // Case 1. 287 return; 288 } 289 else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated 290 && aOriginal && 'line' in aOriginal && 'column' in aOriginal 291 && aGenerated.line > 0 && aGenerated.column >= 0 292 && aOriginal.line > 0 && aOriginal.column >= 0 293 && aSource) { 294 // Cases 2 and 3. 295 return; 296 } 297 else { 298 throw new Error('Invalid mapping: ' + JSON.stringify({ 299 generated: aGenerated, 300 source: aSource, 301 original: aOriginal, 302 name: aName 303 })); 304 } 305 }; 306 307/** 308 * Serialize the accumulated mappings in to the stream of base 64 VLQs 309 * specified by the source map format. 310 */ 311SourceMapGenerator.prototype._serializeMappings = 312 function SourceMapGenerator_serializeMappings() { 313 var previousGeneratedColumn = 0; 314 var previousGeneratedLine = 1; 315 var previousOriginalColumn = 0; 316 var previousOriginalLine = 0; 317 var previousName = 0; 318 var previousSource = 0; 319 var result = ''; 320 var next; 321 var mapping; 322 var nameIdx; 323 var sourceIdx; 324 325 var mappings = this._mappings.toArray(); 326 for (var i = 0, len = mappings.length; i < len; i++) { 327 mapping = mappings[i]; 328 next = '' 329 330 if (mapping.generatedLine !== previousGeneratedLine) { 331 previousGeneratedColumn = 0; 332 while (mapping.generatedLine !== previousGeneratedLine) { 333 next += ';'; 334 previousGeneratedLine++; 335 } 336 } 337 else { 338 if (i > 0) { 339 if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) { 340 continue; 341 } 342 next += ','; 343 } 344 } 345 346 next += base64VLQ.encode(mapping.generatedColumn 347 - previousGeneratedColumn); 348 previousGeneratedColumn = mapping.generatedColumn; 349 350 if (mapping.source != null) { 351 sourceIdx = this._sources.indexOf(mapping.source); 352 next += base64VLQ.encode(sourceIdx - previousSource); 353 previousSource = sourceIdx; 354 355 // lines are stored 0-based in SourceMap spec version 3 356 next += base64VLQ.encode(mapping.originalLine - 1 357 - previousOriginalLine); 358 previousOriginalLine = mapping.originalLine - 1; 359 360 next += base64VLQ.encode(mapping.originalColumn 361 - previousOriginalColumn); 362 previousOriginalColumn = mapping.originalColumn; 363 364 if (mapping.name != null) { 365 nameIdx = this._names.indexOf(mapping.name); 366 next += base64VLQ.encode(nameIdx - previousName); 367 previousName = nameIdx; 368 } 369 } 370 371 result += next; 372 } 373 374 return result; 375 }; 376 377SourceMapGenerator.prototype._generateSourcesContent = 378 function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { 379 return aSources.map(function (source) { 380 if (!this._sourcesContents) { 381 return null; 382 } 383 if (aSourceRoot != null) { 384 source = util.relative(aSourceRoot, source); 385 } 386 var key = util.toSetString(source); 387 return Object.prototype.hasOwnProperty.call(this._sourcesContents, key) 388 ? this._sourcesContents[key] 389 : null; 390 }, this); 391 }; 392 393/** 394 * Externalize the source map. 395 */ 396SourceMapGenerator.prototype.toJSON = 397 function SourceMapGenerator_toJSON() { 398 var map = { 399 version: this._version, 400 sources: this._sources.toArray(), 401 names: this._names.toArray(), 402 mappings: this._serializeMappings() 403 }; 404 if (this._file != null) { 405 map.file = this._file; 406 } 407 if (this._sourceRoot != null) { 408 map.sourceRoot = this._sourceRoot; 409 } 410 if (this._sourcesContents) { 411 map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); 412 } 413 414 return map; 415 }; 416 417/** 418 * Render the source map being generated to a string. 419 */ 420SourceMapGenerator.prototype.toString = 421 function SourceMapGenerator_toString() { 422 return JSON.stringify(this.toJSON()); 423 }; 424 425exports.SourceMapGenerator = SourceMapGenerator; 426