/* -*- Mode: js; js-indent-level: 2; -*- */ /* * Copyright 2011 Mozilla Foundation and contributors * Licensed under the New BSD license. See LICENSE or: * http://opensource.org/licenses/BSD-3-Clause */ const SourceMapGenerator = require('./source-map-generator').SourceMapGenerator const util = require('./util') // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). const REGEX_NEWLINE = /(\r?\n)/ // Newline character code for charCodeAt() comparisons const NEWLINE_CODE = 10 // Private symbol for identifying `SourceNode`s when multiple versions of // the source-map library are loaded. This MUST NOT CHANGE across // versions! const isSourceNode = '$$$isSourceNode$$$' /** * SourceNodes provide a way to abstract over interpolating/concatenating * snippets of generated JavaScript source code while maintaining the line and * column information associated with the original source code. * * @param aLine The original line number. * @param aColumn The original column number. * @param aSource The original source's filename. * @param aChunks Optional. An array of strings which are snippets of * generated JS, or other SourceNodes. * @param aName The original identifier. */ class SourceNode { constructor(aLine, aColumn, aSource, aChunks, aName) { this.children = [] this.sourceContents = {} this.line = aLine == null ? null : aLine this.column = aColumn == null ? null : aColumn this.source = aSource == null ? null : aSource this.name = aName == null ? null : aName this[isSourceNode] = true if (aChunks != null) this.add(aChunks) } /** * Creates a SourceNode from generated code and a SourceMapConsumer. * * @param aGeneratedCode The generated code * @param aSourceMapConsumer The SourceMap for the generated code * @param aRelativePath Optional. The path that relative sources in the * SourceMapConsumer should be relative to. */ static fromStringWithSourceMap( aGeneratedCode, aSourceMapConsumer, aRelativePath ) { // The SourceNode we want to fill with the generated code // and the SourceMap const node = new SourceNode() // All even indices of this array are one line of the generated code, // while all odd indices are the newlines between two adjacent lines // (since `REGEX_NEWLINE` captures its match). // Processed fragments are accessed by calling `shiftNextLine`. const remainingLines = aGeneratedCode.split(REGEX_NEWLINE) let remainingLinesIndex = 0 const shiftNextLine = function () { const lineContents = getNextLine() // The last line of a file might not have a newline. const newLine = getNextLine() || '' return lineContents + newLine function getNextLine() { return remainingLinesIndex < remainingLines.length ? remainingLines[remainingLinesIndex++] : undefined } } // We need to remember the position of "remainingLines" let lastGeneratedLine = 1, lastGeneratedColumn = 0 // The generate SourceNodes we need a code range. // To extract it current and last mapping is used. // Here we store the last mapping. let lastMapping = null let nextLine aSourceMapConsumer.eachMapping(function (mapping) { if (lastMapping !== null) { // We add the code from "lastMapping" to "mapping": // First check if there is a new line in between. if (lastGeneratedLine < mapping.generatedLine) { // Associate first line with "lastMapping" addMappingWithCode(lastMapping, shiftNextLine()) lastGeneratedLine++ lastGeneratedColumn = 0 // The remaining code is added without mapping } else { // There is no new line in between. // Associate the code between "lastGeneratedColumn" and // "mapping.generatedColumn" with "lastMapping" nextLine = remainingLines[remainingLinesIndex] || '' const code = nextLine.substr( 0, mapping.generatedColumn - lastGeneratedColumn ) remainingLines[remainingLinesIndex] = nextLine.substr( mapping.generatedColumn - lastGeneratedColumn ) lastGeneratedColumn = mapping.generatedColumn addMappingWithCode(lastMapping, code) // No more remaining code, continue lastMapping = mapping return } } // We add the generated code until the first mapping // to the SourceNode without any mapping. // Each line is added as separate string. while (lastGeneratedLine < mapping.generatedLine) { node.add(shiftNextLine()) lastGeneratedLine++ } if (lastGeneratedColumn < mapping.generatedColumn) { nextLine = remainingLines[remainingLinesIndex] || '' node.add(nextLine.substr(0, mapping.generatedColumn)) remainingLines[remainingLinesIndex] = nextLine.substr( mapping.generatedColumn ) lastGeneratedColumn = mapping.generatedColumn } lastMapping = mapping }, this) // We have processed all mappings. if (remainingLinesIndex < remainingLines.length) { if (lastMapping) { // Associate the remaining code in the current line with "lastMapping" addMappingWithCode(lastMapping, shiftNextLine()) } // and add the remaining lines without any mapping node.add(remainingLines.splice(remainingLinesIndex).join('')) } // Copy sourcesContent into SourceNode aSourceMapConsumer.sources.forEach(function (sourceFile) { const content = aSourceMapConsumer.sourceContentFor(sourceFile) if (content != null) { if (aRelativePath != null) { sourceFile = util.join(aRelativePath, sourceFile) } node.setSourceContent(sourceFile, content) } }) return node function addMappingWithCode(mapping, code) { if (mapping === null || mapping.source === undefined) { node.add(code) } else { const source = aRelativePath ? util.join(aRelativePath, mapping.source) : mapping.source node.add( new SourceNode( mapping.originalLine, mapping.originalColumn, source, code, mapping.name ) ) } } } /** * Add a chunk of generated JS to this source node. * * @param aChunk A string snippet of generated JS code, another instance of * SourceNode, or an array where each member is one of those things. */ add(aChunk) { if (Array.isArray(aChunk)) { aChunk.forEach(function (chunk) { this.add(chunk) }, this) } else if (aChunk[isSourceNode] || typeof aChunk === 'string') { if (aChunk) { this.children.push(aChunk) } } else { throw new TypeError( 'Expected a SourceNode, string, or an array of SourceNodes and strings. Got ' + aChunk ) } return this } /** * Add a chunk of generated JS to the beginning of this source node. * * @param aChunk A string snippet of generated JS code, another instance of * SourceNode, or an array where each member is one of those things. */ prepend(aChunk) { if (Array.isArray(aChunk)) { for (let i = aChunk.length - 1; i >= 0; i--) { this.prepend(aChunk[i]) } } else if (aChunk[isSourceNode] || typeof aChunk === 'string') { this.children.unshift(aChunk) } else { throw new TypeError( 'Expected a SourceNode, string, or an array of SourceNodes and strings. Got ' + aChunk ) } return this } /** * Walk over the tree of JS snippets in this node and its children. The * walking function is called once for each snippet of JS and is passed that * snippet and the its original associated source's line/column location. * * @param aFn The traversal function. */ walk(aFn) { let chunk for (let i = 0, len = this.children.length; i < len; i++) { chunk = this.children[i] if (chunk[isSourceNode]) { chunk.walk(aFn) } else if (chunk !== '') { aFn(chunk, { source: this.source, line: this.line, column: this.column, name: this.name, }) } } } /** * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between * each of `this.children`. * * @param aSep The separator. */ join(aSep) { let newChildren let i const len = this.children.length if (len > 0) { newChildren = [] for (i = 0; i < len - 1; i++) { newChildren.push(this.children[i]) newChildren.push(aSep) } newChildren.push(this.children[i]) this.children = newChildren } return this } /** * Call String.prototype.replace on the very right-most source snippet. Useful * for trimming whitespace from the end of a source node, etc. * * @param aPattern The pattern to replace. * @param aReplacement The thing to replace the pattern with. */ replaceRight(aPattern, aReplacement) { const lastChild = this.children[this.children.length - 1] if (lastChild[isSourceNode]) { lastChild.replaceRight(aPattern, aReplacement) } else if (typeof lastChild === 'string') { this.children[this.children.length - 1] = lastChild.replace( aPattern, aReplacement ) } else { this.children.push(''.replace(aPattern, aReplacement)) } return this } /** * Set the source content for a source file. This will be added to the SourceMapGenerator * in the sourcesContent field. * * @param aSourceFile The filename of the source file * @param aSourceContent The content of the source file */ setSourceContent(aSourceFile, aSourceContent) { this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent } /** * Walk over the tree of SourceNodes. The walking function is called for each * source file content and is passed the filename and source content. * * @param aFn The traversal function. */ walkSourceContents(aFn) { for (let i = 0, len = this.children.length; i < len; i++) { if (this.children[i][isSourceNode]) { this.children[i].walkSourceContents(aFn) } } const sources = Object.keys(this.sourceContents) for (let i = 0, len = sources.length; i < len; i++) { aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]) } } /** * Return the string representation of this source node. Walks over the tree * and concatenates all the various snippets together to one string. */ toString() { let str = '' this.walk(function (chunk) { str += chunk }) return str } /** * Returns the string representation of this source node along with a source * map. */ toStringWithSourceMap(aArgs) { const generated = { code: '', line: 1, column: 0, } const map = new SourceMapGenerator(aArgs) let sourceMappingActive = false let lastOriginalSource = null let lastOriginalLine = null let lastOriginalColumn = null let lastOriginalName = null this.walk(function (chunk, original) { generated.code += chunk if ( original.source !== null && original.line !== null && original.column !== null ) { if ( lastOriginalSource !== original.source || lastOriginalLine !== original.line || lastOriginalColumn !== original.column || lastOriginalName !== original.name ) { map.addMapping({ source: original.source, original: { line: original.line, column: original.column, }, generated: { line: generated.line, column: generated.column, }, name: original.name, }) } lastOriginalSource = original.source lastOriginalLine = original.line lastOriginalColumn = original.column lastOriginalName = original.name sourceMappingActive = true } else if (sourceMappingActive) { map.addMapping({ generated: { line: generated.line, column: generated.column, }, }) lastOriginalSource = null sourceMappingActive = false } for (let idx = 0, length = chunk.length; idx < length; idx++) { if (chunk.charCodeAt(idx) === NEWLINE_CODE) { generated.line++ generated.column = 0 // Mappings end at eol if (idx + 1 === length) { lastOriginalSource = null sourceMappingActive = false } else if (sourceMappingActive) { map.addMapping({ source: original.source, original: { line: original.line, column: original.column, }, generated: { line: generated.line, column: generated.column, }, name: original.name, }) } } else { generated.column++ } } }) this.walkSourceContents(function (sourceFile, sourceContent) { map.setSourceContent(sourceFile, sourceContent) }) return { code: generated.code, map } } } exports.SourceNode = SourceNode