Add source map support
This commit is contained in:
parent
541ab8334d
commit
7073d18f23
|
@ -1,5 +1,5 @@
|
|||
(function() {
|
||||
var Lexer, compile, ext, extensions, fs, lexer, loadFile, parser, path, vm, _i, _len,
|
||||
var Lexer, SourceMap, compile, count, ext, extensions, fs, lexer, loadFile, parser, path, vm, _i, _len,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
|
||||
__hasProp = {}.hasOwnProperty;
|
||||
|
||||
|
@ -11,8 +11,12 @@
|
|||
|
||||
parser = require('./parser').parser;
|
||||
|
||||
SourceMap = require('./sourcemap').SourceMap;
|
||||
|
||||
vm = require('vm');
|
||||
|
||||
count = require('./helpers').count;
|
||||
|
||||
extensions = ['.coffee', '.litcoffee'];
|
||||
|
||||
loadFile = function(module, filename) {
|
||||
|
@ -36,13 +40,28 @@
|
|||
exports.helpers = require('./helpers');
|
||||
|
||||
exports.compile = compile = function(code, options) {
|
||||
var footer, js, merge;
|
||||
var currentColumn, currentLine, footer, fragment, fragments, js, merge, newLines, _j, _len1;
|
||||
if (options == null) {
|
||||
options = {};
|
||||
}
|
||||
merge = exports.helpers.merge;
|
||||
try {
|
||||
js = (parser.parse(lexer.tokenize(code, options))).compile(options);
|
||||
fragments = (parser.parse(lexer.tokenize(code, options))).compileToFragments(options);
|
||||
currentLine = 0;
|
||||
currentColumn = 0;
|
||||
js = "";
|
||||
for (_j = 0, _len1 = fragments.length; _j < _len1; _j++) {
|
||||
fragment = fragments[_j];
|
||||
if (options.sourceMap) {
|
||||
if (fragment.locationData) {
|
||||
options.sourceMap.addMapping([fragment.locationData.first_line, fragment.locationData.first_column], [currentLine, currentColumn]);
|
||||
}
|
||||
newLines = count(fragment.code, "\n");
|
||||
currentLine += newLines;
|
||||
currentColumn = fragment.code.length - (newLines ? fragment.code.lastIndexOf("\n") : 0);
|
||||
}
|
||||
js += fragment.code;
|
||||
}
|
||||
if (!options.header) {
|
||||
return js;
|
||||
}
|
||||
|
@ -56,6 +75,24 @@
|
|||
return "" + js + "\n// " + footer + "\n";
|
||||
};
|
||||
|
||||
exports.sourceMap = function(code, options) {
|
||||
var merge;
|
||||
if (options == null) {
|
||||
options = {};
|
||||
}
|
||||
merge = exports.helpers.merge;
|
||||
try {
|
||||
options.sourceMap = new SourceMap();
|
||||
exports.compile(code, options);
|
||||
return options.sourceMap;
|
||||
} catch (err) {
|
||||
if (options.filename) {
|
||||
err.message = "In " + options.filename + ", " + err.message;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
exports.tokens = function(code, options) {
|
||||
return lexer.tokenize(code, options);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(function() {
|
||||
var BANNER, CoffeeScript, EventEmitter, SWITCHES, coffee_exts, compileJoin, compileOptions, compilePath, compileScript, compileStdio, exec, exists, forkNode, fs, helpers, hidden, joinTimeout, lint, notSources, optionParser, optparse, opts, outputPath, parseOptions, path, printLine, printTokens, printWarn, removeSource, sourceCode, sources, spawn, timeLog, unwatchDir, usage, version, wait, watch, watchDir, watchers, writeJs, _ref,
|
||||
var BANNER, CoffeeScript, EventEmitter, SWITCHES, coffee_exts, compileJoin, compileOptions, compilePath, compileScript, compileStdio, exec, exists, forkNode, fs, helpers, hidden, joinTimeout, lint, notSources, optionParser, optparse, opts, outputPath, parseOptions, path, printLine, printTokens, printWarn, removeSource, sourceCode, sourcemap, sources, spawn, timeLog, unwatchDir, usage, version, wait, watch, watchDir, watchers, writeJs, _ref,
|
||||
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
||||
|
||||
fs = require('fs');
|
||||
|
@ -12,6 +12,8 @@
|
|||
|
||||
CoffeeScript = require('./coffee-script');
|
||||
|
||||
sourcemap = require('./sourcemap');
|
||||
|
||||
_ref = require('child_process'), spawn = _ref.spawn, exec = _ref.exec;
|
||||
|
||||
EventEmitter = require('events').EventEmitter;
|
||||
|
@ -34,7 +36,7 @@
|
|||
|
||||
BANNER = 'Usage: coffee [options] path/to/script.coffee -- [args]\n\nIf called without options, `coffee` will run your script.';
|
||||
|
||||
SWITCHES = [['-b', '--bare', 'compile without a top-level function wrapper'], ['-c', '--compile', 'compile to JavaScript and save as .js files'], ['-e', '--eval', 'pass a string from the command line as input'], ['-h', '--help', 'display this help message'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'], ['-l', '--lint', 'pipe the compiled JavaScript through JavaScript Lint'], ['-n', '--nodes', 'print out the parse tree that the parser produces'], ['--nodejs [ARGS]', 'pass options directly to the "node" binary'], ['-o', '--output [DIR]', 'set the output directory for compiled JavaScript'], ['-p', '--print', 'print out the compiled JavaScript'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-t', '--tokens', 'print out the tokens that the lexer/rewriter produce'], ['-v', '--version', 'display the version number'], ['-w', '--watch', 'watch scripts for changes and rerun commands']];
|
||||
SWITCHES = [['-b', '--bare', 'compile without a top-level function wrapper'], ['-c', '--compile', 'compile to JavaScript and save as .js files'], ['-e', '--eval', 'pass a string from the command line as input'], ['-h', '--help', 'display this help message'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling'], ['-l', '--lint', 'pipe the compiled JavaScript through JavaScript Lint'], ['-m', '--maps', 'generate source map and save as .map files'], ['-n', '--nodes', 'print out the parse tree that the parser produces'], ['--nodejs [ARGS]', 'pass options directly to the "node" binary'], ['-o', '--output [DIR]', 'set the output directory for compiled JavaScript'], ['-p', '--print', 'print out the compiled JavaScript'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-t', '--tokens', 'print out the tokens that the lexer/rewriter produce'], ['-v', '--version', 'display the version number'], ['-w', '--watch', 'watch scripts for changes and rerun commands']];
|
||||
|
||||
opts = {};
|
||||
|
||||
|
@ -182,8 +184,8 @@
|
|||
CoffeeScript.emit('success', task);
|
||||
if (o.print) {
|
||||
return printLine(t.output.trim());
|
||||
} else if (o.compile) {
|
||||
return writeJs(t.file, t.output, base);
|
||||
} else if (o.compile || o.maps) {
|
||||
return writeJs(base, t.file, t.output, t.options.sourceMap);
|
||||
} else if (o.lint) {
|
||||
return lint(t.file, t.output);
|
||||
}
|
||||
|
@ -374,30 +376,49 @@
|
|||
}
|
||||
};
|
||||
|
||||
outputPath = function(source, base) {
|
||||
outputPath = function(source, base, extension) {
|
||||
var baseDir, dir, filename, srcDir;
|
||||
filename = path.basename(source, path.extname(source)) + '.js';
|
||||
if (extension == null) {
|
||||
extension = ".js";
|
||||
}
|
||||
filename = path.basename(source, path.extname(source)) + extension;
|
||||
srcDir = path.dirname(source);
|
||||
baseDir = base === '.' ? srcDir : srcDir.substring(base.length);
|
||||
dir = opts.output ? path.join(opts.output, baseDir) : srcDir;
|
||||
return path.join(dir, filename);
|
||||
};
|
||||
|
||||
writeJs = function(source, js, base) {
|
||||
var compile, jsDir, jsPath;
|
||||
jsPath = outputPath(source, base);
|
||||
writeJs = function(base, sourcePath, js, sourceMap) {
|
||||
var compile, jsDir, jsPath, sourceMapPath;
|
||||
if (sourceMap == null) {
|
||||
sourceMap = null;
|
||||
}
|
||||
jsPath = outputPath(sourcePath, base);
|
||||
sourceMapPath = outputPath(sourcePath, base, ".map");
|
||||
jsDir = path.dirname(jsPath);
|
||||
compile = function() {
|
||||
if (js.length <= 0) {
|
||||
js = ' ';
|
||||
}
|
||||
return fs.writeFile(jsPath, js, function(err) {
|
||||
if (err) {
|
||||
return printLine(err.message);
|
||||
} else if (opts.compile && opts.watch) {
|
||||
return timeLog("compiled " + source);
|
||||
if (opts.compile) {
|
||||
if (js.length <= 0) {
|
||||
js = ' ';
|
||||
}
|
||||
});
|
||||
if (sourceMap) {
|
||||
js = ("//@ sourceMappingURL=" + (path.basename(sourceMapPath)) + "\n") + js;
|
||||
}
|
||||
fs.writeFile(jsPath, js, function(err) {
|
||||
if (err) {
|
||||
return printLine(err.message);
|
||||
} else if (opts.compile && opts.watch) {
|
||||
return timeLog("compiled " + sourcePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sourceMap) {
|
||||
return fs.writeFile(sourceMapPath, sourcemap.generateV3SourceMap(sourceMap), function(err) {
|
||||
if (err) {
|
||||
return printLine("Could not write source map: " + err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return exists(jsDir, function(itExists) {
|
||||
if (itExists) {
|
||||
|
@ -451,7 +472,7 @@
|
|||
optionParser = new optparse.OptionParser(SWITCHES, BANNER);
|
||||
o = opts = optionParser.parse(process.argv.slice(2));
|
||||
o.compile || (o.compile = !!o.output);
|
||||
o.run = !(o.compile || o.print || o.lint);
|
||||
o.run = !(o.compile || o.print || o.lint || o.maps);
|
||||
o.print = !!(o.print || (o["eval"] || o.stdio && o.compile));
|
||||
sources = o["arguments"];
|
||||
for (i = _i = 0, _len = sources.length; _i < _len; i = ++_i) {
|
||||
|
@ -461,11 +482,13 @@
|
|||
};
|
||||
|
||||
compileOptions = function(filename) {
|
||||
var literate;
|
||||
var literate, sourceMap;
|
||||
literate = path.extname(filename) === '.litcoffee';
|
||||
sourceMap = opts.maps ? new sourcemap.SourceMap() : void 0;
|
||||
return {
|
||||
filename: filename,
|
||||
literate: literate,
|
||||
sourceMap: sourceMap,
|
||||
bare: opts.bare,
|
||||
header: opts.compile
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
function CodeFragment(parent, code) {
|
||||
var _ref2;
|
||||
this.code = code;
|
||||
this.code = "" + code;
|
||||
this.locationData = parent != null ? parent.locationData : void 0;
|
||||
this.type = (parent != null ? (_ref2 = parent.constructor) != null ? _ref2.name : void 0 : void 0) || 'unknown';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
(function() {
|
||||
var BASE64_CHARS, LineMapping, MAX_BASE64_VALUE, VLQ_CONTINUATION_BIT, VLQ_SHIFT, VLQ_VALUE_MASK, decodeBase64Char, encodeBase64Char;
|
||||
|
||||
LineMapping = (function() {
|
||||
|
||||
function LineMapping(generatedLine) {
|
||||
this.generatedLine = generatedLine;
|
||||
this.columnMap = {};
|
||||
this.columnMappings = [];
|
||||
}
|
||||
|
||||
LineMapping.prototype.addMapping = function(generatedColumn, _arg) {
|
||||
var sourceColumn, sourceLine;
|
||||
sourceLine = _arg[0], sourceColumn = _arg[1];
|
||||
if (this.columnMap[generatedColumn]) {
|
||||
return;
|
||||
}
|
||||
this.columnMap[generatedColumn] = {
|
||||
generatedLine: this.generatedLine,
|
||||
generatedColumn: generatedColumn,
|
||||
sourceLine: sourceLine,
|
||||
sourceColumn: sourceColumn
|
||||
};
|
||||
this.columnMappings.push(this.columnMap[generatedColumn]);
|
||||
return this.columnMappings.sort(function(a, b) {
|
||||
return a.generatedColumn - b.generatedColumn;
|
||||
});
|
||||
};
|
||||
|
||||
LineMapping.prototype.getSourcePosition = function(generatedColumn) {
|
||||
var answer, columnMapping, lastColumnMapping, _i, _len, _ref;
|
||||
answer = null;
|
||||
lastColumnMapping = null;
|
||||
_ref = this.columnMappings;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
columnMapping = _ref[_i];
|
||||
if (columnMapping.generatedColumn > generatedColumn) {
|
||||
break;
|
||||
} else {
|
||||
lastColumnMapping = columnMapping;
|
||||
}
|
||||
}
|
||||
if (lastColumnMapping) {
|
||||
return answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn];
|
||||
}
|
||||
};
|
||||
|
||||
return LineMapping;
|
||||
|
||||
})();
|
||||
|
||||
exports.SourceMap = (function() {
|
||||
|
||||
function SourceMap() {
|
||||
this.generatedLines = [];
|
||||
}
|
||||
|
||||
SourceMap.prototype.addMapping = function(sourceLocation, generatedLocation) {
|
||||
var generatedColumn, generatedLine, lineMapping;
|
||||
generatedLine = generatedLocation[0], generatedColumn = generatedLocation[1];
|
||||
lineMapping = this.generatedLines[generatedLine];
|
||||
if (!lineMapping) {
|
||||
lineMapping = this.generatedLines[generatedLine] = new LineMapping(generatedLine);
|
||||
}
|
||||
return lineMapping.addMapping(generatedColumn, sourceLocation);
|
||||
};
|
||||
|
||||
SourceMap.prototype.getSourcePosition = function(_arg) {
|
||||
var answer, generatedColumn, generatedLine, lineMapping;
|
||||
generatedLine = _arg[0], generatedColumn = _arg[1];
|
||||
answer = null;
|
||||
lineMapping = this.generatedLines[generatedLine];
|
||||
if (!lineMapping) {
|
||||
|
||||
} else {
|
||||
answer = lineMapping.getSourcePosition(generatedColumn);
|
||||
}
|
||||
return answer;
|
||||
};
|
||||
|
||||
SourceMap.prototype.forEachMapping = function(fn) {
|
||||
var columnMapping, generatedLineNumber, lineMapping, _i, _len, _ref, _results;
|
||||
_ref = this.generatedLines;
|
||||
_results = [];
|
||||
for (generatedLineNumber = _i = 0, _len = _ref.length; _i < _len; generatedLineNumber = ++_i) {
|
||||
lineMapping = _ref[generatedLineNumber];
|
||||
if (lineMapping) {
|
||||
_results.push((function() {
|
||||
var _j, _len1, _ref1, _results1;
|
||||
_ref1 = lineMapping.columnMappings;
|
||||
_results1 = [];
|
||||
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
|
||||
columnMapping = _ref1[_j];
|
||||
_results1.push(fn(columnMapping));
|
||||
}
|
||||
return _results1;
|
||||
})());
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
return SourceMap;
|
||||
|
||||
})();
|
||||
|
||||
exports.generateV3SourceMap = function(sourceMap, sourceFile, generatedFile) {
|
||||
var answer, lastGeneratedColumnWritten, lastSourceColumnWritten, lastSourceLineWritten, mappings, needComma, writingGeneratedLine;
|
||||
if (sourceFile == null) {
|
||||
sourceFile = null;
|
||||
}
|
||||
if (generatedFile == null) {
|
||||
generatedFile = null;
|
||||
}
|
||||
writingGeneratedLine = 0;
|
||||
lastGeneratedColumnWritten = 0;
|
||||
lastSourceLineWritten = 0;
|
||||
lastSourceColumnWritten = 0;
|
||||
needComma = false;
|
||||
mappings = "";
|
||||
sourceMap.forEachMapping(function(mapping) {
|
||||
while (writingGeneratedLine < mapping.generatedLine) {
|
||||
lastGeneratedColumnWritten = 0;
|
||||
needComma = false;
|
||||
mappings += ";";
|
||||
writingGeneratedLine++;
|
||||
}
|
||||
if (needComma) {
|
||||
mappings += ",";
|
||||
needComma = false;
|
||||
}
|
||||
mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten);
|
||||
lastGeneratedColumnWritten = mapping.generatedColumn;
|
||||
mappings += exports.vlqEncodeValue(0);
|
||||
mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten);
|
||||
lastSourceLineWritten = mapping.sourceLine;
|
||||
mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten);
|
||||
lastSourceColumnWritten = mapping.sourceColumn;
|
||||
return needComma = true;
|
||||
});
|
||||
answer = {
|
||||
version: 3,
|
||||
file: generatedFile,
|
||||
sourceRoot: "",
|
||||
source: [sourceFile],
|
||||
names: [],
|
||||
mappings: mappings
|
||||
};
|
||||
return JSON.stringify(answer);
|
||||
};
|
||||
|
||||
exports.loadV3SourceMap = function(sourceMap) {
|
||||
return todo();
|
||||
};
|
||||
|
||||
BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
MAX_BASE64_VALUE = BASE64_CHARS.length - 1;
|
||||
|
||||
encodeBase64Char = function(value) {
|
||||
if (value > MAX_BASE64_VALUE) {
|
||||
throw new Error("Cannot encode value " + value + " > " + MAX_BASE64_VALUE);
|
||||
} else if (value < 0) {
|
||||
throw new Error("Cannot encode value " + value + " < 0");
|
||||
}
|
||||
return BASE64_CHARS[value];
|
||||
};
|
||||
|
||||
decodeBase64Char = function(char) {
|
||||
var value;
|
||||
value = BASE64_CHARS.indexOf(char);
|
||||
if (value === -1) {
|
||||
throw new Error("Invalid Base 64 character: " + char);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
VLQ_SHIFT = 5;
|
||||
|
||||
VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT;
|
||||
|
||||
VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1;
|
||||
|
||||
exports.vlqEncodeValue = function(value) {
|
||||
var answer, nextVlqChunk, signBit, valueToEncode;
|
||||
signBit = value < 0 ? 1 : 0;
|
||||
valueToEncode = (Math.abs(value) << 1) + signBit;
|
||||
answer = "";
|
||||
while (valueToEncode || !answer) {
|
||||
nextVlqChunk = valueToEncode & VLQ_VALUE_MASK;
|
||||
valueToEncode = valueToEncode >> VLQ_SHIFT;
|
||||
if (valueToEncode) {
|
||||
nextVlqChunk |= VLQ_CONTINUATION_BIT;
|
||||
}
|
||||
answer += encodeBase64Char(nextVlqChunk);
|
||||
}
|
||||
return answer;
|
||||
};
|
||||
|
||||
exports.vlqDecodeValue = function(str, offset) {
|
||||
var consumed, continuationShift, done, nextChunkValue, nextVlqChunk, position, signBit, value;
|
||||
if (offset == null) {
|
||||
offset = 0;
|
||||
}
|
||||
position = offset;
|
||||
done = false;
|
||||
value = 0;
|
||||
continuationShift = 0;
|
||||
while (!done) {
|
||||
nextVlqChunk = decodeBase64Char(str[position]);
|
||||
position += 1;
|
||||
nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK;
|
||||
value += nextChunkValue << continuationShift;
|
||||
if (!(nextVlqChunk & VLQ_CONTINUATION_BIT)) {
|
||||
done = true;
|
||||
}
|
||||
continuationShift += VLQ_SHIFT;
|
||||
}
|
||||
consumed = position - offset;
|
||||
signBit = value & 1;
|
||||
value = value >> 1;
|
||||
if (signBit) {
|
||||
value = -value;
|
||||
}
|
||||
return [value, consumed];
|
||||
};
|
||||
|
||||
}).call(this);
|
||||
|
||||
// Generated by CoffeeScript 1.5.0-pre
|
|
@ -6,11 +6,13 @@
|
|||
# If included on a webpage, it will automatically sniff out, compile, and
|
||||
# execute all scripts present in `text/coffeescript` tags.
|
||||
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
{Lexer} = require './lexer'
|
||||
{parser} = require './parser'
|
||||
vm = require 'vm'
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
{Lexer} = require './lexer'
|
||||
{parser} = require './parser'
|
||||
{SourceMap} = require './sourcemap'
|
||||
vm = require 'vm'
|
||||
{count} = require './helpers'
|
||||
|
||||
# The file extensions that are considered to be CoffeeScript.
|
||||
extensions = ['.coffee', '.litcoffee']
|
||||
|
@ -36,7 +38,25 @@ exports.helpers = require './helpers'
|
|||
exports.compile = compile = (code, options = {}) ->
|
||||
{merge} = exports.helpers
|
||||
try
|
||||
js = (parser.parse lexer.tokenize(code, options)).compile options
|
||||
fragments = (parser.parse lexer.tokenize(code, options)).compileToFragments options
|
||||
|
||||
currentLine = 0
|
||||
currentColumn = 0
|
||||
js = ""
|
||||
for fragment in fragments
|
||||
# Update the sourcemap with data from each fragment
|
||||
if options.sourceMap
|
||||
if fragment.locationData
|
||||
options.sourceMap.addMapping(
|
||||
[fragment.locationData.first_line, fragment.locationData.first_column],
|
||||
[currentLine, currentColumn])
|
||||
newLines = count fragment.code, "\n"
|
||||
currentLine += newLines
|
||||
currentColumn = fragment.code.length - (if newLines then fragment.code.lastIndexOf "\n" else 0)
|
||||
|
||||
# Copy the code from each fragment into the final JavaScript.
|
||||
js += fragment.code
|
||||
|
||||
return js unless options.header
|
||||
catch err
|
||||
err.message = "In #{options.filename}, #{err.message}" if options.filename
|
||||
|
@ -44,6 +64,19 @@ exports.compile = compile = (code, options = {}) ->
|
|||
footer = "Generated by CoffeeScript #{@VERSION}"
|
||||
"#{js}\n// #{footer}\n"
|
||||
|
||||
# Generates a source map for a string of CoffeeScript code.
|
||||
# Returns a SourceMap object.
|
||||
exports.sourceMap = (code, options = {}) ->
|
||||
{merge} = exports.helpers
|
||||
try
|
||||
options.sourceMap = new SourceMap()
|
||||
exports.compile code, options
|
||||
return options.sourceMap
|
||||
catch err
|
||||
err.message = "In #{options.filename}, #{err.message}" if options.filename
|
||||
throw err
|
||||
|
||||
|
||||
# Tokenize a string of CoffeeScript code, and return the array of tokens.
|
||||
exports.tokens = (code, options) ->
|
||||
lexer.tokenize code, options
|
||||
|
|
|
@ -10,6 +10,7 @@ path = require 'path'
|
|||
helpers = require './helpers'
|
||||
optparse = require './optparse'
|
||||
CoffeeScript = require './coffee-script'
|
||||
sourcemap = require './sourcemap'
|
||||
{spawn, exec} = require 'child_process'
|
||||
{EventEmitter} = require 'events'
|
||||
|
||||
|
@ -39,6 +40,7 @@ SWITCHES = [
|
|||
['-i', '--interactive', 'run an interactive CoffeeScript REPL']
|
||||
['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling']
|
||||
['-l', '--lint', 'pipe the compiled JavaScript through JavaScript Lint']
|
||||
['-m', '--maps', 'generate source map and save as .map files']
|
||||
['-n', '--nodes', 'print out the parse tree that the parser produces']
|
||||
[ '--nodejs [ARGS]', 'pass options directly to the "node" binary']
|
||||
['-o', '--output [DIR]', 'set the output directory for compiled JavaScript']
|
||||
|
@ -131,9 +133,11 @@ compileScript = (file, input, base) ->
|
|||
compileJoin()
|
||||
else
|
||||
t.output = CoffeeScript.compile t.input, t.options
|
||||
|
||||
CoffeeScript.emit 'success', task
|
||||
if o.print then printLine t.output.trim()
|
||||
else if o.compile then writeJs t.file, t.output, base
|
||||
else if o.compile || o.maps
|
||||
writeJs base, t.file, t.output, t.options.sourceMap
|
||||
else if o.lint then lint t.file, t.output
|
||||
catch err
|
||||
CoffeeScript.emit 'failure', err, task
|
||||
|
@ -247,8 +251,8 @@ removeSource = (source, base, removeJs) ->
|
|||
timeLog "removed #{source}"
|
||||
|
||||
# Get the corresponding output JavaScript path for a source file.
|
||||
outputPath = (source, base) ->
|
||||
filename = path.basename(source, path.extname(source)) + '.js'
|
||||
outputPath = (source, base, extension=".js") ->
|
||||
filename = path.basename(source, path.extname(source)) + extension
|
||||
srcDir = path.dirname source
|
||||
baseDir = if base is '.' then srcDir else srcDir.substring base.length
|
||||
dir = if opts.output then path.join opts.output, baseDir else srcDir
|
||||
|
@ -257,16 +261,27 @@ outputPath = (source, base) ->
|
|||
# Write out a JavaScript source file with the compiled code. By default, files
|
||||
# are written out in `cwd` as `.js` files with the same name, but the output
|
||||
# directory can be customized with `--output`.
|
||||
writeJs = (source, js, base) ->
|
||||
jsPath = outputPath source, base
|
||||
#
|
||||
# If source maps were also requested, this will write `.map` files into the same
|
||||
# directory as the `.js` files.
|
||||
writeJs = (base, sourcePath, js, sourceMap = null) ->
|
||||
jsPath = outputPath sourcePath, base
|
||||
sourceMapPath = outputPath sourcePath, base, ".map"
|
||||
|
||||
jsDir = path.dirname jsPath
|
||||
compile = ->
|
||||
js = ' ' if js.length <= 0
|
||||
fs.writeFile jsPath, js, (err) ->
|
||||
if err
|
||||
printLine err.message
|
||||
else if opts.compile and opts.watch
|
||||
timeLog "compiled #{source}"
|
||||
if opts.compile
|
||||
js = ' ' if js.length <= 0
|
||||
if sourceMap then js = "//@ sourceMappingURL=#{path.basename sourceMapPath}\n" + js
|
||||
fs.writeFile jsPath, js, (err) ->
|
||||
if err
|
||||
printLine err.message
|
||||
else if opts.compile and opts.watch
|
||||
timeLog "compiled #{sourcePath}"
|
||||
if sourceMap
|
||||
fs.writeFile sourceMapPath, (sourcemap.generateV3SourceMap sourceMap), (err) ->
|
||||
if err
|
||||
printLine "Could not write source map: #{err.message}"
|
||||
exists jsDir, (itExists) ->
|
||||
if itExists then compile() else exec "mkdir -p #{jsDir}", compile
|
||||
|
||||
|
@ -303,7 +318,7 @@ parseOptions = ->
|
|||
optionParser = new optparse.OptionParser SWITCHES, BANNER
|
||||
o = opts = optionParser.parse process.argv[2..]
|
||||
o.compile or= !!o.output
|
||||
o.run = not (o.compile or o.print or o.lint)
|
||||
o.run = not (o.compile or o.print or o.lint or o.maps)
|
||||
o.print = !! (o.print or (o.eval or o.stdio and o.compile))
|
||||
sources = o.arguments
|
||||
sourceCode[i] = null for source, i in sources
|
||||
|
@ -312,7 +327,8 @@ parseOptions = ->
|
|||
# The compile-time options to pass to the CoffeeScript compiler.
|
||||
compileOptions = (filename) ->
|
||||
literate = path.extname(filename) is '.litcoffee'
|
||||
{filename, literate, bare: opts.bare, header: opts.compile}
|
||||
sourceMap = if opts.maps then new sourcemap.SourceMap()
|
||||
{filename, literate, sourceMap, bare: opts.bare, header: opts.compile}
|
||||
|
||||
# Start up a new Node.js instance with the arguments in `--nodejs` passed to
|
||||
# the `node` binary, preserving the other options.
|
||||
|
|
|
@ -33,7 +33,8 @@ PARANOID = false
|
|||
# came from. CodeFragments can be assembled together into working code just by catting together
|
||||
# all the CodeFragments' `code` snippets, in order.
|
||||
exports.CodeFragment = class CodeFragment
|
||||
constructor: (parent, @code) ->
|
||||
constructor: (parent, code) ->
|
||||
@code = "#{code}"
|
||||
@locationData = parent?.locationData
|
||||
@type = parent?.constructor?.name or 'unknown'
|
||||
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
#### LineMapping
|
||||
|
||||
# Hold data about mappings for one line of generated source code.
|
||||
|
||||
class LineMapping
|
||||
constructor: (@generatedLine) ->
|
||||
# columnMap keeps track of which columns we've already mapped.
|
||||
@columnMap = {}
|
||||
|
||||
# columnMappings is an array of all column mappings, sorted by generated-column.
|
||||
@columnMappings = []
|
||||
|
||||
addMapping: (generatedColumn, [sourceLine, sourceColumn]) ->
|
||||
if @columnMap[generatedColumn]
|
||||
# We already have a mapping for this column.
|
||||
return
|
||||
|
||||
@columnMap[generatedColumn] = {
|
||||
generatedLine: @generatedLine
|
||||
generatedColumn
|
||||
sourceLine
|
||||
sourceColumn
|
||||
}
|
||||
|
||||
@columnMappings.push @columnMap[generatedColumn]
|
||||
@columnMappings.sort (a,b) -> a.generatedColumn - b.generatedColumn
|
||||
|
||||
getSourcePosition: (generatedColumn) ->
|
||||
answer = null
|
||||
lastColumnMapping = null
|
||||
for columnMapping in @columnMappings
|
||||
if columnMapping.generatedColumn > generatedColumn
|
||||
break
|
||||
else
|
||||
lastColumnMapping = columnMapping
|
||||
if lastColumnMapping
|
||||
answer = [lastColumnMapping.sourceLine, lastColumnMapping.sourceColumn]
|
||||
|
||||
#### SourceMap
|
||||
|
||||
# Maps locations in a generated source file back to locations in the original source file.
|
||||
#
|
||||
# This is intentionally agnostic towards how a source map might be represented on disk. A
|
||||
# SourceMap can be converted to a "v3" style sourcemap with `#generateV3SourceMap()`, for example
|
||||
# but the SourceMap class itself knows nothing about v3 source maps.
|
||||
|
||||
class exports.SourceMap
|
||||
constructor: () ->
|
||||
# `generatedLines` is an array of LineMappings, one per generated line.
|
||||
@generatedLines = []
|
||||
|
||||
# Adds a mapping to this SourceMap.
|
||||
#
|
||||
# `sourceLocation` and `generatedLocation` are both [line, column] arrays.
|
||||
# If there is already a mapping for the specified `generatedLine` and
|
||||
# `generatedColumn`, then this will have no effect.
|
||||
addMapping: (sourceLocation, generatedLocation) ->
|
||||
[generatedLine, generatedColumn] = generatedLocation
|
||||
|
||||
lineMapping = @generatedLines[generatedLine]
|
||||
if not lineMapping
|
||||
lineMapping = @generatedLines[generatedLine] = new LineMapping(generatedLine)
|
||||
|
||||
lineMapping.addMapping generatedColumn, sourceLocation
|
||||
|
||||
# Returns [sourceLine, sourceColumn], or null if no mapping could be found.
|
||||
getSourcePosition: ([generatedLine, generatedColumn]) ->
|
||||
answer = null
|
||||
lineMapping = @generatedLines[generatedLine]
|
||||
if not lineMapping
|
||||
# TODO: Search backwards for the line?
|
||||
else
|
||||
answer = lineMapping.getSourcePosition generatedColumn
|
||||
|
||||
answer
|
||||
|
||||
|
||||
# `fn` will be called once for every recorded mapping, in the order in
|
||||
# which they occur in the generated source. `fn` will be passed an object
|
||||
# with four properties: sourceLine, sourceColumn, generatedLine, and
|
||||
# generatedColumn.
|
||||
forEachMapping: (fn) ->
|
||||
for lineMapping, generatedLineNumber in @generatedLines
|
||||
if lineMapping
|
||||
for columnMapping in lineMapping.columnMappings
|
||||
fn(columnMapping)
|
||||
|
||||
|
||||
#### generateV3SourceMap
|
||||
|
||||
# Builds a V3 source map from a SourceMap object.
|
||||
# Returns the generated JSON as a string.
|
||||
|
||||
exports.generateV3SourceMap = (sourceMap, sourceFile=null, generatedFile=null) ->
|
||||
writingGeneratedLine = 0
|
||||
lastGeneratedColumnWritten = 0
|
||||
lastSourceLineWritten = 0
|
||||
lastSourceColumnWritten = 0
|
||||
needComma = no
|
||||
|
||||
mappings = ""
|
||||
|
||||
sourceMap.forEachMapping (mapping) ->
|
||||
while writingGeneratedLine < mapping.generatedLine
|
||||
lastGeneratedColumnWritten = 0
|
||||
needComma = no
|
||||
mappings += ";"
|
||||
writingGeneratedLine++
|
||||
|
||||
# Write a comma if we've already written a segment on this line.
|
||||
if needComma
|
||||
mappings += ","
|
||||
needComma = no
|
||||
|
||||
# Write the next segment.
|
||||
# Segments can be 1, 4, or 5 values. If just one, then it is a generated column which
|
||||
# doesn't match anything in the source code.
|
||||
#
|
||||
# Fields are all zero-based, and relative to the previous occurence unless otherwise noted:
|
||||
# * starting-column in generated source, relative to previous occurence for the current line.
|
||||
# * index into the "sources" list
|
||||
# * starting line in the original source
|
||||
# * starting column in the original source
|
||||
# * index into the "names" list associated with this segment.
|
||||
|
||||
# Add the generated start-column
|
||||
mappings += exports.vlqEncodeValue(mapping.generatedColumn - lastGeneratedColumnWritten)
|
||||
lastGeneratedColumnWritten = mapping.generatedColumn
|
||||
|
||||
# Add the index into the sources list
|
||||
mappings += exports.vlqEncodeValue(0)
|
||||
|
||||
# Add the source start-line
|
||||
mappings += exports.vlqEncodeValue(mapping.sourceLine - lastSourceLineWritten)
|
||||
lastSourceLineWritten = mapping.sourceLine
|
||||
|
||||
# Add the source start-column
|
||||
mappings += exports.vlqEncodeValue(mapping.sourceColumn - lastSourceColumnWritten)
|
||||
lastSourceColumnWritten = mapping.sourceColumn
|
||||
|
||||
# TODO: Do we care about symbol names for CoffeeScript? Probably not.
|
||||
|
||||
needComma = yes
|
||||
|
||||
answer = {
|
||||
version: 3
|
||||
file: generatedFile
|
||||
sourceRoot: ""
|
||||
source: [sourceFile]
|
||||
names: []
|
||||
mappings
|
||||
}
|
||||
|
||||
return JSON.stringify answer
|
||||
|
||||
# Load a SourceMap from a JSON string. Returns the SourceMap object.
|
||||
exports.loadV3SourceMap = (sourceMap) ->
|
||||
todo()
|
||||
|
||||
#### Base64 encoding helpers
|
||||
|
||||
BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
MAX_BASE64_VALUE = BASE64_CHARS.length - 1
|
||||
|
||||
encodeBase64Char = (value) ->
|
||||
if value > MAX_BASE64_VALUE
|
||||
throw new Error "Cannot encode value #{value} > #{MAX_BASE64_VALUE}"
|
||||
else if value < 0
|
||||
throw new Error "Cannot encode value #{value} < 0"
|
||||
BASE64_CHARS[value]
|
||||
|
||||
decodeBase64Char = (char) ->
|
||||
value = BASE64_CHARS.indexOf char
|
||||
if value == -1
|
||||
throw new Error "Invalid Base 64 character: #{char}"
|
||||
value
|
||||
|
||||
#### Base 64 VLQ encoding/decoding helpers
|
||||
|
||||
# Note that SourceMap VLQ encoding is "backwards". MIDI style VLQ encoding puts the
|
||||
# most-significant-bit (MSB) from the original value into the MSB of the VLQ encoded value
|
||||
# (see http://en.wikipedia.org/wiki/File:Uintvar_coding.svg). SourceMap VLQ does things
|
||||
# the other way around, with the least significat four bits of the original value encoded
|
||||
# into the first byte of the VLQ encoded value.
|
||||
|
||||
VLQ_SHIFT = 5
|
||||
VLQ_CONTINUATION_BIT = 1 << VLQ_SHIFT # 0010 0000
|
||||
VLQ_VALUE_MASK = VLQ_CONTINUATION_BIT - 1 # 0001 1111
|
||||
|
||||
# Encode a value as Base 64 VLQ.
|
||||
exports.vlqEncodeValue = (value) ->
|
||||
# Least significant bit represents the sign.
|
||||
signBit = if value < 0 then 1 else 0
|
||||
|
||||
# Next bits are the actual value
|
||||
valueToEncode = (Math.abs(value) << 1) + signBit
|
||||
|
||||
answer = ""
|
||||
# Make sure we encode at least one character, even if valueToEncode is 0.
|
||||
while valueToEncode || !answer
|
||||
nextVlqChunk = valueToEncode & VLQ_VALUE_MASK
|
||||
valueToEncode = valueToEncode >> VLQ_SHIFT
|
||||
|
||||
if valueToEncode
|
||||
nextVlqChunk |= VLQ_CONTINUATION_BIT
|
||||
|
||||
answer += encodeBase64Char(nextVlqChunk)
|
||||
|
||||
return answer
|
||||
|
||||
# Decode a Base 64 VLQ value.
|
||||
#
|
||||
# Returns `[value, consumed]` where `value` is the decoded value, and `consumed` is the number
|
||||
# of characters consumed from `str`.
|
||||
exports.vlqDecodeValue = (str, offset=0) ->
|
||||
position = offset
|
||||
done = false
|
||||
|
||||
value = 0
|
||||
continuationShift = 0
|
||||
|
||||
while !done
|
||||
nextVlqChunk = decodeBase64Char(str[position])
|
||||
position += 1
|
||||
|
||||
nextChunkValue = nextVlqChunk & VLQ_VALUE_MASK
|
||||
value += (nextChunkValue << continuationShift)
|
||||
|
||||
if !(nextVlqChunk & VLQ_CONTINUATION_BIT)
|
||||
# We'll be done after this character.
|
||||
done = true
|
||||
|
||||
# Bits are encoded least-significant first (opposite of MIDI VLQ). Increase the
|
||||
# continuationShift, so the next byte will end up where it should in the value.
|
||||
continuationShift += VLQ_SHIFT
|
||||
|
||||
consumed = position - offset
|
||||
|
||||
# Least significant bit represents the sign.
|
||||
signBit = value & 1
|
||||
value = value >> 1
|
||||
|
||||
if signBit then value = -value
|
||||
|
||||
return [value, consumed]
|
|
@ -0,0 +1,39 @@
|
|||
sourcemap = require '../src/sourcemap'
|
||||
|
||||
vlqEncodedValues = [
|
||||
[1, "C"],
|
||||
[-1, "D"],
|
||||
[2, "E"],
|
||||
[-2, "F"],
|
||||
[0, "A"],
|
||||
[16, "gB"],
|
||||
[948, "o7B"]
|
||||
]
|
||||
|
||||
test "vlqEncodeValue tests", ->
|
||||
for pair in vlqEncodedValues
|
||||
eq (sourcemap.vlqEncodeValue pair[0]), pair[1]
|
||||
|
||||
test "vlqDecodeValue tests", ->
|
||||
for pair in vlqEncodedValues
|
||||
arrayEq (sourcemap.vlqDecodeValue pair[1]), [pair[0], pair[1].length]
|
||||
|
||||
test "vlqDecodeValue with offset", ->
|
||||
for pair in vlqEncodedValues
|
||||
# Try with an offset, and some cruft at the end.
|
||||
arrayEq (sourcemap.vlqDecodeValue ("abc" + pair[1] + "efg"), 3), [pair[0], pair[1].length]
|
||||
|
||||
test "SourceMap tests", ->
|
||||
map = new sourcemap.SourceMap()
|
||||
map.addMapping [0, 0], [0, 0]
|
||||
map.addMapping [1, 5], [2, 4]
|
||||
map.addMapping [1, 6], [2, 7]
|
||||
map.addMapping [1, 9], [2, 8]
|
||||
map.addMapping [3, 0], [3, 4]
|
||||
eq (sourcemap.generateV3SourceMap map, "source.coffee", "source.js"), '{"version":3,"file":"source.js","sourceRoot":"","source":["source.coffee"],"names":[],"mappings":"AAAA;;IACK,GAAC,CAAG;IAET"}'
|
||||
|
||||
# Look up a generated column - should get back the original source position.
|
||||
arrayEq map.getSourcePosition([2,8]), [1,9]
|
||||
|
||||
# Look up a point futher along on the same line - should get back the same source position.
|
||||
arrayEq map.getSourcePosition([2,10]), [1,9]
|
Loading…
Reference in New Issue