#! /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'); } }