emscripten/tools/source-maps/sourcemapper.js

209 строки
7.4 KiB
JavaScript
Executable File

#! /usr/bin/env node
"use strict";
var fs = require('fs');
var path = require('path');
var START_MARKER = '// EMSCRIPTEN_START_FUNCS\n';
var END_MARKER = '// EMSCRIPTEN_END_FUNCS\n';
function countLines(s) {
var count = 0;
for (var i = 0, l = s.length; i < l; i ++) {
if (s[i] === '\n') count++;
}
return count;
}
// For a minor optimization, only do win32->unix normalization if we are actually on Windows,
// which avoids redundantly scanning files if not needed.
var isWindows = (process.platform === 'win32');
var unixPathRe = new RegExp('\\\\', 'g');
// Returns the given (possibly Windows) path p normalized to unix path separators '/'.
function toUnixPath(p) {
if (isWindows) {
return p.replace(unixPathRe, '/');
} else {
return p;
}
}
var unixLineEndRe = new RegExp('\r\n', 'g');
// Returns the given (possibly Windows) text data t normalized to unix line endings '\n'.
function toUnixLineEnding(t) {
if (isWindows) {
return t.replace(unixLineEndRe, '\n');
} else {
return t;
}
}
// If path "p2" is a relative path, joins paths p1 and p2 to form "p1/p2". If p2 is an absolute path, "p2" is returned.
function joinPath(p1, p2) {
if (p2[0] == '/' || (p2.length >= 3 && p2[1] == ':' && (p2[2] == '/' || p2[2] == '\\'))) // Is p2 an absolute path?
return p2;
else
return toUnixPath(path.join(p1, p2));
}
/*
* Extracts the line (not block) comments from the generated function code and
* invokes commentHandler with (comment content, line number of comment). This
* function implements a simplistic lexer with the following assumptions:
* 1. "// EMSCRIPTEN_START_FUNCS" and "// EMSCRIPTEN_END_FUNCS" are unique
* markers for separating library code from generated function code. Things
* will break if they appear as part of a string in library code (but OK if
* they occur in generated code).
* 2. Between these two markers, no regexes are used.
*/
function extractComments(source, commentHandler) {
var state = 'code';
var commentContent = '';
var functionStartIdx = source.indexOf(START_MARKER);
var functionEndIdx = source.lastIndexOf(END_MARKER);
var lineCount = countLines(source.slice(0, functionStartIdx)) + 2;
for (var i = functionStartIdx + START_MARKER.length; i < functionEndIdx; i++) {
var c = source[i];
var nextC = source[i+1];
switch (state) {
case 'code':
if (c === '/') {
if (nextC === '/') { state = 'lineComment'; i++; }
else if (nextC === '*') { state = 'blockComment'; i++; }
}
else if (c === '"') state = 'doubleQuotedString';
else if (c === '\'') state = 'singleQuotedString';
break;
case 'lineComment':
if (c === '\n') {
state = 'code';
commentHandler(commentContent, lineCount);
commentContent = "";
} else {
commentContent += c;
}
break;
case 'blockComment':
if (c === '*' && nextC === '/') state = 'code';
case 'singleQuotedString':
if (c === '\\') i++;
else if (c === '\'') state = 'code';
break;
case 'doubleQuotedString':
if (c === '\\') i++;
else if (c === '"') state = 'code';
break;
}
if (c === '\n') lineCount++;
}
}
function getMappings(source) {
// generatedLineNumber -> { originalLineNumber, originalFileName }
var mappings = {};
extractComments(source, function(content, generatedLineNumber) {
var matches = /@line (\d+)(?: "([^"]*)")?/.exec(content);
if (matches === null) return;
var originalFileName = matches[2];
mappings[generatedLineNumber] = {
originalLineNumber: parseInt(matches[1], 10),
originalFileName: originalFileName
}
});
return mappings;
}
function generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset) {
var SourceMapGenerator = require('source-map').SourceMapGenerator;
var generator = new SourceMapGenerator({ file: mapFileBaseName });
var seenFiles = Object.create(null);
for (var generatedLineNumber in mappings) {
var generatedLineNumber = parseInt(generatedLineNumber, 10);
var mapping = mappings[generatedLineNumber];
var originalFileName = mapping.originalFileName;
generator.addMapping({
generated: { line: generatedLineNumber + generatedLineOffset, column: 0 },
original: { line: mapping.originalLineNumber, column: 0 },
source: originalFileName
});
// we could call setSourceContent repeatedly, but readFileSync is slow, so
// avoid doing it unnecessarily
if (!(originalFileName in seenFiles)) {
seenFiles[originalFileName] = true;
var rootedPath = joinPath(sourceRoot, originalFileName);
try {
generator.setSourceContent(originalFileName, fs.readFileSync(rootedPath, 'utf-8'));
} catch (e) {
console.warn("sourcemapper: Unable to find original file for " + originalFileName +
" at " + rootedPath);
}
}
}
fs.writeFileSync(mapFileBaseName + '.map', generator.toString());
}
function appendMappingURL(fileName, source, mapFileName) {
var lastLine = source.slice(source.lastIndexOf('\n'));
if (!/sourceMappingURL/.test(lastLine))
fs.appendFileSync(fileName, '//# sourceMappingURL=' + path.basename(mapFileName));
}
function parseArgs(args) {
var rv = { _: [] }; // unflagged args go into `_`; similar to the optimist library
for (var i = 0; i < args.length; i++) {
if (/^--/.test(args[i])) rv[args[i].slice(2)] = args[++i];
else rv._.push(args[i]);
}
return rv;
}
if (require.main === module) {
if (process.argv.length < 3) {
console.log('Usage: ./sourcemapper.js <original js> <optimized js file ...> \\\n' +
'\t--sourceRoot <default "."> \\\n' +
'\t--mapFileBaseName <default `filename`> \\\n' +
'\t--offset <default 0>');
process.exit(1);
} else {
var opts = parseArgs(process.argv.slice(2));
var fileName = opts._[0];
var sourceRoot = opts.sourceRoot ? toUnixPath(opts.sourceRoot) : ".";
var mapFileBaseName = toUnixPath(opts.mapFileBaseName ? opts.mapFileBaseName : fileName);
var generatedLineOffset = opts.offset ? parseInt(opts.offset, 10) : 0;
var generatedSource = toUnixLineEnding(fs.readFileSync(fileName, 'utf-8'));
var source = generatedSource;
var mappings = getMappings(generatedSource);
for (var i = 1, l = opts._.length; i < l; i ++) {
var optimizedSource = toUnixLineEnding(fs.readFileSync(opts._[i], 'utf-8'))
var optimizedMappings = getMappings(optimizedSource);
var newMappings = {};
// uglify processes the code between EMSCRIPTEN_START_FUNCS and
// EMSCRIPTEN_END_FUNCS, so its line number maps are relative to those
// markers. we correct for that here. +2 = 1 for the newline in the marker
// and 1 to make it a 1-based index.
var startFuncsLineNumber = countLines(source.slice(0, source.indexOf(START_MARKER))) + 2;
for (var line in optimizedMappings) {
var originalLineNumber = optimizedMappings[line].originalLineNumber + startFuncsLineNumber;
if (originalLineNumber in mappings) {
newMappings[line] = mappings[originalLineNumber];
}
}
mappings = newMappings;
source = optimizedSource;
}
generateMap(mappings, sourceRoot, mapFileBaseName, generatedLineOffset);
appendMappingURL(opts._[opts._.length - 1], generatedSource,
opts.mapFileBaseName + '.map');
}
}