diff options
Diffstat (limited to 'tools/source-maps/sourcemapper.js')
-rwxr-xr-x | tools/source-maps/sourcemapper.js | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/tools/source-maps/sourcemapper.js b/tools/source-maps/sourcemapper.js new file mode 100755 index 00000000..fa908900 --- /dev/null +++ b/tools/source-maps/sourcemapper.js @@ -0,0 +1,177 @@ +#! /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; +} + +/* + * 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 = originalFileName[0] === path.sep ? + originalFileName : path.join(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 ? opts.sourceRoot : "."; + var mapFileBaseName = opts.mapFileBaseName ? opts.mapFileBaseName : fileName; + var generatedLineOffset = opts.offset ? parseInt(opts.offset, 10) : 0; + + var generatedSource = fs.readFileSync(fileName, 'utf-8'); + var source = generatedSource; + var mappings = getMappings(generatedSource); + for (var i = 1, l = opts._.length; i < l; i ++) { + var optimizedSource = 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'); + } +} |