diff options
author | Jez Ng <me@jezng.com> | 2013-06-06 17:46:13 -0700 |
---|---|---|
committer | Jez Ng <me@jezng.com> | 2013-06-19 01:22:01 -0700 |
commit | 747f74e1055feaea11222978d46c421ac92602ea (patch) | |
tree | 2c153da61e78bb6a6d2ee9731a15787b01b81474 | |
parent | 86e672a4371e57238fabdde3a35eeee03c8aa77f (diff) |
Implement basic source maps. Closes #1252.
-rwxr-xr-x | emcc | 9 | ||||
-rwxr-xr-x | tools/sourcemapper.js | 118 |
2 files changed, 126 insertions, 1 deletions
@@ -49,7 +49,7 @@ emcc can be influenced by a few environment variables: import os, sys, shutil, tempfile, subprocess, shlex, time, re, logging from subprocess import PIPE, STDOUT -from tools import shared +from tools import shared, jsrun from tools.shared import Compression, execute, suffix, unsuffixed, unsuffixed_basename from tools.response_file import read_response_file @@ -1585,12 +1585,17 @@ try: else: logging.debug('did not see memory initialization') + def generate_source_maps(filename, mapFileBaseName): + jsrun.run_js(shared.path_from_root('tools', 'sourcemapper.js'), + shared.NODE_JS, [filename, os.getcwd(), mapFileBaseName]) + # If we were asked to also generate HTML, do that if final_suffix == 'html': logging.debug('generating HTML') shell = open(shell_path).read() html = open(target, 'w') if not Compression.on: + if keep_js_debug: generate_source_maps(final, target) html.write(shell.replace('{{{ SCRIPT_CODE }}}', open(final).read())) else: # Compress the main code @@ -1661,6 +1666,8 @@ try: from tools.split import split_javascript_file split_javascript_file(final, unsuffixed(target), split_js_file) else: + if keep_js_debug: generate_source_maps(final, target) + # copy final JS to output shutil.move(final, target) diff --git a/tools/sourcemapper.js b/tools/sourcemapper.js new file mode 100755 index 00000000..1f28b371 --- /dev/null +++ b/tools/sourcemapper.js @@ -0,0 +1,118 @@ +#! /usr/bin/env node + +"use strict"; + +function countLines(s) { + var count = 0; + for (var i = 0, l = s.length; i < l; i ++) { + var c = s[i]; + if (c === '\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('// EMSCRIPTEN_START_FUNCS'); + var functionEndIdx = source.lastIndexOf('// EMSCRIPTEN_END_FUNCS'); + var lineCount = countLines(source.slice(0, functionStartIdx)); + + for (var i = functionStartIdx; i < functionEndIdx; i++) { + var c = source[i]; + var nextC = source[i+1]; + if (c === '\n') lineCount++; + 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; + } + } +} + +function generateMap(fileName, sourceRoot, mapFileBaseName) { + var fs = require('fs'); + var path = require('path'); + var SourceMapGenerator = require('source-map').SourceMapGenerator; + + var generator = new SourceMapGenerator({ file: fileName }); + var generatedSource = fs.readFileSync(fileName, 'utf-8'); + var seenFiles = Object.create(null); + + extractComments(generatedSource, function(content, generatedLineNumber) { + console.log(content, generatedLineNumber) + var matches = /@line (\d+) "([^"]*)"/.exec(content); + if (matches === null) return; + var originalLineNumber = parseInt(matches[1], 10); + var originalFileName = matches[2]; + + if (!(originalFileName in seenFiles)) { + seenFiles[originalFileName] = true; + try { + generator.setSourceContent(originalFileName, + fs.readFileSync(sourceRoot + "/" + originalFileName)); + } catch (e) { + console.warn("Unable to find original file for " + originalFileName); + } + } + + generator.addMapping({ + generated: { line: generatedLineNumber, column: 0 }, + original: { line: originalLineNumber, column: 0 }, + source: originalFileName + }); + }); + + var mapFileName = mapFileBaseName + '.map'; + fs.writeFileSync(mapFileName, generator.toString()); + + var lastLine = generatedSource.slice(generatedSource.lastIndexOf('\n')); + if (!/sourceMappingURL/.test(lastLine)) + fs.appendFileSync(fileName, '//@ sourceMappingURL=' + path.basename(mapFileName)); +} + +if (require.main === module) { + if (process.argv.length < 3) { + console.log('Usage: ./sourcemapper.js <filename> <source root (default: .)> ' + + '<map file basename (default: filename)>'); + process.exit(1); + } else { + var sourceRoot = process.argv.length > 3 ? process.argv[3] : "."; + var mapFileBaseName = process.argv.length > 4 ? process.argv[4] : process.argv[2]; + generateMap(process.argv[2], sourceRoot, mapFileBaseName); + } +} |