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