source-node.js (13793B)
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 SourceMapGenerator = require("./source-map-generator").SourceMapGenerator; 9 const util = require("./util"); 10 11 // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other 12 // operating systems these days (capturing the result). 13 const REGEX_NEWLINE = /(\r?\n)/; 14 15 // Newline character code for charCodeAt() comparisons 16 const NEWLINE_CODE = 10; 17 18 // Private symbol for identifying `SourceNode`s when multiple versions of 19 // the source-map library are loaded. This MUST NOT CHANGE across 20 // versions! 21 const isSourceNode = "$$$isSourceNode$$$"; 22 23 /** 24 * SourceNodes provide a way to abstract over interpolating/concatenating 25 * snippets of generated JavaScript source code while maintaining the line and 26 * column information associated with the original source code. 27 * 28 * @param aLine The original line number. 29 * @param aColumn The original column number. 30 * @param aSource The original source's filename. 31 * @param aChunks Optional. An array of strings which are snippets of 32 * generated JS, or other SourceNodes. 33 * @param aName The original identifier. 34 */ 35 class SourceNode { 36 constructor(aLine, aColumn, aSource, aChunks, aName) { 37 this.children = []; 38 this.sourceContents = {}; 39 this.line = aLine == null ? null : aLine; 40 this.column = aColumn == null ? null : aColumn; 41 this.source = aSource == null ? null : aSource; 42 this.name = aName == null ? null : aName; 43 this[isSourceNode] = true; 44 if (aChunks != null) this.add(aChunks); 45 } 46 47 /** 48 * Creates a SourceNode from generated code and a SourceMapConsumer. 49 * 50 * @param aGeneratedCode The generated code 51 * @param aSourceMapConsumer The SourceMap for the generated code 52 * @param aRelativePath Optional. The path that relative sources in the 53 * SourceMapConsumer should be relative to. 54 */ 55 static fromStringWithSourceMap( 56 aGeneratedCode, 57 aSourceMapConsumer, 58 aRelativePath 59 ) { 60 // The SourceNode we want to fill with the generated code 61 // and the SourceMap 62 const node = new SourceNode(); 63 64 // All even indices of this array are one line of the generated code, 65 // while all odd indices are the newlines between two adjacent lines 66 // (since `REGEX_NEWLINE` captures its match). 67 // Processed fragments are accessed by calling `shiftNextLine`. 68 const remainingLines = aGeneratedCode.split(REGEX_NEWLINE); 69 let remainingLinesIndex = 0; 70 const shiftNextLine = function () { 71 const lineContents = getNextLine(); 72 // The last line of a file might not have a newline. 73 const newLine = getNextLine() || ""; 74 return lineContents + newLine; 75 76 function getNextLine() { 77 return remainingLinesIndex < remainingLines.length 78 ? remainingLines[remainingLinesIndex++] 79 : undefined; 80 } 81 }; 82 83 // We need to remember the position of "remainingLines" 84 let lastGeneratedLine = 1, 85 lastGeneratedColumn = 0; 86 87 // The generate SourceNodes we need a code range. 88 // To extract it current and last mapping is used. 89 // Here we store the last mapping. 90 let lastMapping = null; 91 let nextLine; 92 93 aSourceMapConsumer.eachMapping(function (mapping) { 94 if (lastMapping !== null) { 95 // We add the code from "lastMapping" to "mapping": 96 // First check if there is a new line in between. 97 if (lastGeneratedLine < mapping.generatedLine) { 98 // Associate first line with "lastMapping" 99 addMappingWithCode(lastMapping, shiftNextLine()); 100 lastGeneratedLine++; 101 lastGeneratedColumn = 0; 102 // The remaining code is added without mapping 103 } else { 104 // There is no new line in between. 105 // Associate the code between "lastGeneratedColumn" and 106 // "mapping.generatedColumn" with "lastMapping" 107 nextLine = remainingLines[remainingLinesIndex] || ""; 108 const code = nextLine.substr( 109 0, 110 mapping.generatedColumn - lastGeneratedColumn 111 ); 112 remainingLines[remainingLinesIndex] = nextLine.substr( 113 mapping.generatedColumn - lastGeneratedColumn 114 ); 115 lastGeneratedColumn = mapping.generatedColumn; 116 addMappingWithCode(lastMapping, code); 117 // No more remaining code, continue 118 lastMapping = mapping; 119 return; 120 } 121 } 122 // We add the generated code until the first mapping 123 // to the SourceNode without any mapping. 124 // Each line is added as separate string. 125 while (lastGeneratedLine < mapping.generatedLine) { 126 node.add(shiftNextLine()); 127 lastGeneratedLine++; 128 } 129 if (lastGeneratedColumn < mapping.generatedColumn) { 130 nextLine = remainingLines[remainingLinesIndex] || ""; 131 node.add(nextLine.substr(0, mapping.generatedColumn)); 132 remainingLines[remainingLinesIndex] = nextLine.substr( 133 mapping.generatedColumn 134 ); 135 lastGeneratedColumn = mapping.generatedColumn; 136 } 137 lastMapping = mapping; 138 }, this); 139 // We have processed all mappings. 140 if (remainingLinesIndex < remainingLines.length) { 141 if (lastMapping) { 142 // Associate the remaining code in the current line with "lastMapping" 143 addMappingWithCode(lastMapping, shiftNextLine()); 144 } 145 // and add the remaining lines without any mapping 146 node.add(remainingLines.splice(remainingLinesIndex).join("")); 147 } 148 149 // Copy sourcesContent into SourceNode 150 aSourceMapConsumer.sources.forEach(function (sourceFile) { 151 const content = aSourceMapConsumer.sourceContentFor(sourceFile); 152 if (content != null) { 153 if (aRelativePath != null) { 154 sourceFile = util.join(aRelativePath, sourceFile); 155 } 156 node.setSourceContent(sourceFile, content); 157 } 158 }); 159 160 return node; 161 162 function addMappingWithCode(mapping, code) { 163 if (mapping === null || mapping.source === undefined) { 164 node.add(code); 165 } else { 166 const source = aRelativePath 167 ? util.join(aRelativePath, mapping.source) 168 : mapping.source; 169 node.add( 170 new SourceNode( 171 mapping.originalLine, 172 mapping.originalColumn, 173 source, 174 code, 175 mapping.name 176 ) 177 ); 178 } 179 } 180 } 181 182 /** 183 * Add a chunk of generated JS to this source node. 184 * 185 * @param aChunk A string snippet of generated JS code, another instance of 186 * SourceNode, or an array where each member is one of those things. 187 */ 188 add(aChunk) { 189 if (Array.isArray(aChunk)) { 190 aChunk.forEach(function (chunk) { 191 this.add(chunk); 192 }, this); 193 } else if (aChunk[isSourceNode] || typeof aChunk === "string") { 194 if (aChunk) { 195 this.children.push(aChunk); 196 } 197 } else { 198 throw new TypeError( 199 "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + 200 aChunk 201 ); 202 } 203 return this; 204 } 205 206 /** 207 * Add a chunk of generated JS to the beginning of this source node. 208 * 209 * @param aChunk A string snippet of generated JS code, another instance of 210 * SourceNode, or an array where each member is one of those things. 211 */ 212 prepend(aChunk) { 213 if (Array.isArray(aChunk)) { 214 for (let i = aChunk.length - 1; i >= 0; i--) { 215 this.prepend(aChunk[i]); 216 } 217 } else if (aChunk[isSourceNode] || typeof aChunk === "string") { 218 this.children.unshift(aChunk); 219 } else { 220 throw new TypeError( 221 "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + 222 aChunk 223 ); 224 } 225 return this; 226 } 227 228 /** 229 * Walk over the tree of JS snippets in this node and its children. The 230 * walking function is called once for each snippet of JS and is passed that 231 * snippet and the its original associated source's line/column location. 232 * 233 * @param aFn The traversal function. 234 */ 235 walk(aFn) { 236 let chunk; 237 for (let i = 0, len = this.children.length; i < len; i++) { 238 chunk = this.children[i]; 239 if (chunk[isSourceNode]) { 240 chunk.walk(aFn); 241 } else if (chunk !== "") { 242 aFn(chunk, { 243 source: this.source, 244 line: this.line, 245 column: this.column, 246 name: this.name, 247 }); 248 } 249 } 250 } 251 252 /** 253 * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between 254 * each of `this.children`. 255 * 256 * @param aSep The separator. 257 */ 258 join(aSep) { 259 let newChildren; 260 let i; 261 const len = this.children.length; 262 if (len > 0) { 263 newChildren = []; 264 for (i = 0; i < len - 1; i++) { 265 newChildren.push(this.children[i]); 266 newChildren.push(aSep); 267 } 268 newChildren.push(this.children[i]); 269 this.children = newChildren; 270 } 271 return this; 272 } 273 274 /** 275 * Call String.prototype.replace on the very right-most source snippet. Useful 276 * for trimming whitespace from the end of a source node, etc. 277 * 278 * @param aPattern The pattern to replace. 279 * @param aReplacement The thing to replace the pattern with. 280 */ 281 replaceRight(aPattern, aReplacement) { 282 const lastChild = this.children[this.children.length - 1]; 283 if (lastChild[isSourceNode]) { 284 lastChild.replaceRight(aPattern, aReplacement); 285 } else if (typeof lastChild === "string") { 286 this.children[this.children.length - 1] = lastChild.replace( 287 aPattern, 288 aReplacement 289 ); 290 } else { 291 this.children.push("".replace(aPattern, aReplacement)); 292 } 293 return this; 294 } 295 296 /** 297 * Set the source content for a source file. This will be added to the SourceMapGenerator 298 * in the sourcesContent field. 299 * 300 * @param aSourceFile The filename of the source file 301 * @param aSourceContent The content of the source file 302 */ 303 setSourceContent(aSourceFile, aSourceContent) { 304 this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent; 305 } 306 307 /** 308 * Walk over the tree of SourceNodes. The walking function is called for each 309 * source file content and is passed the filename and source content. 310 * 311 * @param aFn The traversal function. 312 */ 313 walkSourceContents(aFn) { 314 for (let i = 0, len = this.children.length; i < len; i++) { 315 if (this.children[i][isSourceNode]) { 316 this.children[i].walkSourceContents(aFn); 317 } 318 } 319 320 const sources = Object.keys(this.sourceContents); 321 for (let i = 0, len = sources.length; i < len; i++) { 322 aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]); 323 } 324 } 325 326 /** 327 * Return the string representation of this source node. Walks over the tree 328 * and concatenates all the various snippets together to one string. 329 */ 330 toString() { 331 let str = ""; 332 this.walk(function (chunk) { 333 str += chunk; 334 }); 335 return str; 336 } 337 338 /** 339 * Returns the string representation of this source node along with a source 340 * map. 341 */ 342 toStringWithSourceMap(aArgs) { 343 const generated = { 344 code: "", 345 line: 1, 346 column: 0, 347 }; 348 const map = new SourceMapGenerator(aArgs); 349 let sourceMappingActive = false; 350 let lastOriginalSource = null; 351 let lastOriginalLine = null; 352 let lastOriginalColumn = null; 353 let lastOriginalName = null; 354 this.walk(function (chunk, original) { 355 generated.code += chunk; 356 if ( 357 original.source !== null && 358 original.line !== null && 359 original.column !== null 360 ) { 361 if ( 362 lastOriginalSource !== original.source || 363 lastOriginalLine !== original.line || 364 lastOriginalColumn !== original.column || 365 lastOriginalName !== original.name 366 ) { 367 map.addMapping({ 368 source: original.source, 369 original: { 370 line: original.line, 371 column: original.column, 372 }, 373 generated: { 374 line: generated.line, 375 column: generated.column, 376 }, 377 name: original.name, 378 }); 379 } 380 lastOriginalSource = original.source; 381 lastOriginalLine = original.line; 382 lastOriginalColumn = original.column; 383 lastOriginalName = original.name; 384 sourceMappingActive = true; 385 } else if (sourceMappingActive) { 386 map.addMapping({ 387 generated: { 388 line: generated.line, 389 column: generated.column, 390 }, 391 }); 392 lastOriginalSource = null; 393 sourceMappingActive = false; 394 } 395 for (let idx = 0, length = chunk.length; idx < length; idx++) { 396 if (chunk.charCodeAt(idx) === NEWLINE_CODE) { 397 generated.line++; 398 generated.column = 0; 399 // Mappings end at eol 400 if (idx + 1 === length) { 401 lastOriginalSource = null; 402 sourceMappingActive = false; 403 } else if (sourceMappingActive) { 404 map.addMapping({ 405 source: original.source, 406 original: { 407 line: original.line, 408 column: original.column, 409 }, 410 generated: { 411 line: generated.line, 412 column: generated.column, 413 }, 414 name: original.name, 415 }); 416 } 417 } else { 418 generated.column++; 419 } 420 } 421 }); 422 this.walkSourceContents(function (sourceFile, sourceContent) { 423 map.setSourceContent(sourceFile, sourceContent); 424 }); 425 426 return { code: generated.code, map }; 427 } 428 } 429 430 exports.SourceNode = SourceNode;