431 lines
13 KiB
JavaScript
431 lines
13 KiB
JavaScript
/* -*- 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
|