Generate source maps

Allow the precompiler to generate source maps when the srcFile parameter is passed.

This refactors large chunks of the code generation pipeline, allowing metadata to be associated with code chunks as well as breaking out much of the code generation logic into a separate helper.
This commit is contained in:
kpdecker
2014-11-06 09:56:00 -06:00
parent 249f559104
commit 3ee0682247
8 changed files with 330 additions and 103 deletions
+151
View File
@@ -0,0 +1,151 @@
import {isArray} from "../utils";
try {
var SourceMap = require('source-map'),
SourceNode = SourceMap.SourceNode;
} catch (err) {
/* istanbul ignore next: tested but not covered in istanbul due to dist build */
SourceNode = function(line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
this.add(chunks);
}
};
/* istanbul ignore next */
SourceNode.prototype = {
add: function(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src += chunks;
},
prepend: function(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src = chunks + this.src;
},
toStringWithSourceMap: function() {
return {code: this.toString()};
},
toString: function() {
return this.src;
}
};
}
function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) {
var ret = [];
for (var i = 0, len = chunk.length; i < len; i++) {
ret.push(codeGen.wrap(chunk[i], loc));
}
return ret;
} else if (typeof chunk === 'boolean' || typeof chunk === 'number') {
// Handle primitives that the SourceNode will throw up on
return chunk+'';
}
return chunk;
}
function CodeGen(srcFile) {
this.srcFile = srcFile;
this.source = [];
}
CodeGen.prototype = {
prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc));
},
push: function(source, loc) {
this.source.push(this.wrap(source, loc));
},
merge: function() {
var source = this.empty();
this.each(function(line) {
source.add([' ', line, '\n']);
});
return source;
},
each: function(iter) {
for (var i = 0, len = this.source.length; i < len; i++) {
iter(this.source[i]);
}
},
empty: function(loc) {
loc = loc || this.currentLocation || {};
return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile);
},
wrap: function(chunk, loc) {
if (chunk instanceof SourceNode) {
return chunk;
}
loc = loc || this.currentLocation || {};
chunk = castChunk(chunk, this, loc);
return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk);
},
functionCall: function(fn, type, params) {
params = this.generateList(params);
return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
},
quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') + '"';
},
objectLiteral: function(obj) {
var pairs = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]);
}
}
var ret = this.generateList(pairs);
ret.prepend('{');
ret.add('}');
return ret;
},
generateList: function(entries, loc) {
var ret = this.empty(loc);
for (var i = 0, len = entries.length; i < len; i++) {
if (i) {
ret.add(',');
}
ret.add(castChunk(entries[i], this, loc));
}
return ret;
},
generateArray: function(entries, loc) {
var ret = this.generateList(entries, loc);
ret.prepend('[');
ret.add(']');
return ret;
}
};
export default CodeGen;
+124 -100
View File
@@ -1,5 +1,7 @@
import { COMPILER_REVISION, REVISION_CHANGES } from "../base";
import Exception from "../exception";
import {isArray} from "../utils";
import CodeGen from "./code-gen";
function Literal(value) {
this.value = value;
@@ -12,15 +14,15 @@ JavaScriptCompiler.prototype = {
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name /* , type*/) {
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return parent + "." + name;
return [parent, ".", name];
} else {
return parent + "['" + name + "']";
return [parent, "['", name, "']"];
}
},
depthedLookup: function(name) {
this.aliases.lookup = 'this.lookup';
return 'lookup(depths, "' + name + '")';
return ['lookup(depths, "', name, '")'];
},
compilerInfo: function() {
@@ -29,15 +31,23 @@ JavaScriptCompiler.prototype = {
return [revision, versions];
},
appendToBuffer: function(string) {
appendToBuffer: function(string, location, explicit) {
// Force a string as this simplifies the merge logic.
if (!isArray(string)) {
string = [string];
}
string = this.source.wrap(string, location);
if (this.environment.isSimple) {
return "return " + string + ";";
return ['return ', string, ';'];
} else if (explicit) {
// This is a case where the buffer operation occurs as a child of another
// construct, generally braces. We have to explicitly output these buffer
// operations to ensure that the emitted code goes in the correct location.
return ['buffer += ', string, ';'];
} else {
return {
appendToBuffer: true,
content: string,
toString: function() { return "buffer += " + string + ";"; }
};
string.appendToBuffer = true;
return string;
}
},
@@ -84,11 +94,12 @@ JavaScriptCompiler.prototype = {
for (i = 0, l = opcodes.length; i < l; i++) {
opcode = opcodes[i];
this.currentLoc = opcode.loc;
this.source.currentLocation = opcode.loc;
this[opcode.opcode].apply(this, opcode.args);
}
// Flush any trailing content that might be pending.
this.source.currentLocation = undefined;
this.pushSource('');
/* istanbul ignore next */
@@ -125,6 +136,13 @@ JavaScriptCompiler.prototype = {
if (!asObject) {
ret.compiler = JSON.stringify(ret.compiler);
ret = this.objectLiteral(ret);
if (options.srcName) {
ret = ret.toStringWithSourceMap({file: options.destName});
ret.map = ret.map && ret.map.toString();
} else {
ret = ret.toString();
}
}
return ret;
@@ -137,7 +155,7 @@ JavaScriptCompiler.prototype = {
// track the last context pushed into place to allow skipping the
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = [];
this.source = new CodeGen(this.options.srcName);
},
createFunctionContext: function(asObject) {
@@ -169,59 +187,67 @@ JavaScriptCompiler.prototype = {
return Function.apply(this, params);
} else {
return 'function(' + params.join(',') + ') {\n ' + source + '}';
return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']);
}
},
mergeSource: function(varDeclarations) {
var source = '',
buffer,
var isSimple = this.environment.isSimple,
appendOnly = !this.forceBuffer,
appendFirst;
appendFirst,
for (var i = 0, len = this.source.length; i < len; i++) {
var line = this.source[i];
sourceSeen,
bufferStart,
bufferEnd;
this.source.each(function(line) {
if (line.appendToBuffer) {
if (buffer) {
buffer = buffer + '\n + ' + line.content;
if (bufferStart) {
line.prepend(' + ');
} else {
buffer = line.content;
bufferStart = line;
}
bufferEnd = line;
} else {
if (buffer) {
if (!source) {
if (bufferStart) {
if (!sourceSeen) {
appendFirst = true;
source = buffer + ';\n ';
} else {
source += 'buffer += ' + buffer + ';\n ';
bufferStart.prepend('buffer += ');
}
buffer = undefined;
bufferEnd.add(';');
bufferStart = bufferEnd = undefined;
}
source += line + '\n ';
if (!this.environment.isSimple) {
sourceSeen = true;
if (!isSimple) {
appendOnly = false;
}
}
}
});
if (appendOnly) {
if (buffer || !source) {
source += 'return ' + (buffer || '""') + ';\n';
if (bufferStart) {
bufferStart.prepend('return ');
bufferEnd.add(';');
} else {
this.source.push('return "";');
}
} else {
varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer());
if (buffer) {
source += 'return buffer + ' + buffer + ';\n';
if (bufferStart) {
bufferStart.prepend('return buffer + ');
bufferEnd.add(';');
} else {
source += 'return buffer;\n';
this.source.push('return buffer;');
}
}
if (varDeclarations) {
source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source;
this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n '));
}
return source;
return this.source.merge();
},
// [blockValue]
@@ -242,7 +268,7 @@ JavaScriptCompiler.prototype = {
var blockName = this.popStack();
params.splice(1, 0, blockName);
this.push('blockHelperMissing.call(' + params.join(', ') + ')');
this.push(this.source.functionCall('blockHelperMissing', 'call', params));
},
// [ambiguousBlockValue]
@@ -263,7 +289,10 @@ JavaScriptCompiler.prototype = {
var current = this.topStack();
params.splice(1, 0, current);
this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
this.pushSource([
'if (!', this.lastHelper, ') { ',
current, ' = ', this.source.functionCall('blockHelperMissing', 'call', params),
'}']);
},
// [appendContent]
@@ -275,6 +304,8 @@ JavaScriptCompiler.prototype = {
appendContent: function(content) {
if (this.pendingContent) {
content = this.pendingContent + content;
} else {
this.pendingLocation = this.source.currentLocation;
}
this.pendingContent = content;
@@ -292,15 +323,15 @@ JavaScriptCompiler.prototype = {
append: function() {
if (this.isInline()) {
this.replaceStack(function(current) {
return ' != null ? ' + current + ' : ""';
return [' != null ? ', current, ' : ""'];
});
this.pushSource(this.appendToBuffer(this.popStack()));
} else {
var local = this.popStack();
this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }');
this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']);
if (this.environment.isSimple) {
this.pushSource("else { " + this.appendToBuffer("''") + " }");
this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']);
}
}
},
@@ -314,7 +345,7 @@ JavaScriptCompiler.prototype = {
appendEscaped: function() {
this.aliases.escapeExpression = 'this.escapeExpression';
this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
this.pushSource(this.appendToBuffer(['escapeExpression(', this.popStack(), ')']));
},
// [getContext]
@@ -364,10 +395,10 @@ JavaScriptCompiler.prototype = {
// We want to ensure that zero and false are handled properly if the context (falsy flag)
// needs to have the special handling for these values.
if (!falsy) {
return ' != null ? ' + lookup + ' : ' + current;
return [' != null ? ', lookup, ' : ', current];
} else {
// Otherwise we can use generic falsy handling
return ' && ' + lookup;
return [' && ', lookup];
}
});
}
@@ -390,7 +421,7 @@ JavaScriptCompiler.prototype = {
var len = parts.length;
for (var i = 0; i < len; i++) {
this.replaceStack(function(current) {
return ' && ' + this.nameLookup(current, parts[i], 'data');
return [' && ', this.nameLookup(current, parts[i], 'data')];
});
}
},
@@ -405,7 +436,7 @@ JavaScriptCompiler.prototype = {
resolvePossibleLambda: function() {
this.aliases.lambda = 'this.lambda';
this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')');
this.push(['lambda(', this.popStack(), ', ', this.contextName(0), ')']);
},
// [pushStringParam]
@@ -453,14 +484,14 @@ JavaScriptCompiler.prototype = {
this.hash = this.hashes.pop();
if (this.trackIds) {
this.push('{' + hash.ids.join(',') + '}');
this.push(this.objectLiteral(hash.ids));
}
if (this.stringParams) {
this.push('{' + hash.contexts.join(',') + '}');
this.push('{' + hash.types.join(',') + '}');
this.push(this.objectLiteral(hash.contexts));
this.push(this.objectLiteral(hash.types));
}
this.push('{\n ' + hash.values.join(',\n ') + '\n }');
this.push(this.objectLiteral(hash.values));
},
// [pushString]
@@ -473,17 +504,6 @@ JavaScriptCompiler.prototype = {
this.pushStackLiteral(this.quotedString(string));
},
// [push]
//
// On stack, before: ...
// On stack, after: expr, ...
//
// Push an expression onto the stack
push: function(expr) {
this.inlineStack.push(expr);
return expr;
},
// [pushLiteral]
//
// On stack, before: ...
@@ -526,9 +546,13 @@ JavaScriptCompiler.prototype = {
var nonHelper = this.popStack();
var helper = this.setupHelper(paramSize, name);
var simple = isSimple ? [helper.name, ' || '] : '';
var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing';
this.push('((' + lookup + ').call(' + helper.callParams + '))');
this.push(
this.source.functionCall(
['('].concat(simple, nonHelper, ' || ', 'helperMissing)'),
'call',
helper.callParams));
},
// [invokeKnownHelper]
@@ -540,7 +564,7 @@ JavaScriptCompiler.prototype = {
// so a `helperMissing` fallback is not required.
invokeKnownHelper: function(paramSize, name) {
var helper = this.setupHelper(paramSize, name);
this.push(helper.name + ".call(" + helper.callParams + ")");
this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
},
// [invokeAmbiguous]
@@ -567,10 +591,12 @@ JavaScriptCompiler.prototype = {
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
this.push(
'((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing'
+ (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),'
+ '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))');
this.push([
'((helper = (helper = ', helperName, ' || ', nonHelper, ') != null ? helper : helperMissing',
(helper.paramsInit ? ['),(', helper.paramsInit] : []), '),',
'(typeof helper === functionType ? ',
this.source.functionCall('helper','call', helper.callParams), ' : helper))'
]);
},
// [invokePartial]
@@ -592,7 +618,7 @@ JavaScriptCompiler.prototype = {
params.push('depths');
}
this.push("this.invokePartial(" + params.join(", ") + ")");
this.push(this.source.functionCall('this.invokePartial', '', params));
},
// [assignToHash]
@@ -617,15 +643,15 @@ JavaScriptCompiler.prototype = {
var hash = this.hash;
if (context) {
hash.contexts.push("'" + key + "': " + context);
hash.contexts[key] = context;
}
if (type) {
hash.types.push("'" + key + "': " + type);
hash.types[key] = type;
}
if (id) {
hash.ids.push("'" + key + "': " + id);
hash.ids[key] = id;
}
hash.values.push("'" + key + "': (" + value + ")");
hash.values[key] = value;
},
pushId: function(type, name) {
@@ -697,13 +723,23 @@ JavaScriptCompiler.prototype = {
}
},
push: function(expr) {
if (!(expr instanceof Literal)) {
expr = this.source.wrap(expr);
}
this.inlineStack.push(expr);
return expr;
},
pushStackLiteral: function(item) {
this.push(new Literal(item));
},
pushSource: function(source) {
if (this.pendingContent) {
this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent)));
this.source.push(
this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation));
this.pendingContent = undefined;
}
@@ -713,7 +749,7 @@ JavaScriptCompiler.prototype = {
},
replaceStack: function(callback) {
var prefix = '',
var prefix = ['('],
inline = this.isInline(),
stack,
createdStack,
@@ -729,14 +765,15 @@ JavaScriptCompiler.prototype = {
if (top instanceof Literal) {
// Literals do not need to be inlined
prefix = stack = top.value;
stack = [top.value];
prefix = ['(', stack];
usedLiteral = true;
} else {
// Get or create the current stack name for use by the inline
createdStack = true;
var name = this.incrStack();
prefix = '(' + this.push(name) + ' = ' + top + ')';
prefix = ['((', this.push(name), ' = ', top, ')'];
stack = this.topStack();
}
@@ -748,7 +785,7 @@ JavaScriptCompiler.prototype = {
if (createdStack) {
this.stackSlot--;
}
this.push('(' + prefix + item + ')');
this.push(prefix.concat(item, ')'));
},
incrStack: function() {
@@ -769,7 +806,7 @@ JavaScriptCompiler.prototype = {
this.compileStack.push(entry);
} else {
var stack = this.incrStack();
this.pushSource(stack + " = " + entry + ";");
this.pushSource([stack, ' = ', entry, ';']);
this.compileStack.push(stack);
}
}
@@ -817,25 +854,11 @@ JavaScriptCompiler.prototype = {
},
quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') + '"';
return this.source.quotedString(str);
},
objectLiteral: function(obj) {
var pairs = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
pairs.push(this.quotedString(key) + ':' + obj[key]);
}
}
return '{' + pairs.join(',') + '}';
return this.source.objectLiteral(obj);
},
setupHelper: function(paramSize, name, blockHelper) {
@@ -847,7 +870,7 @@ JavaScriptCompiler.prototype = {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: [this.contextName(0)].concat(params).join(", ")
callParams: [this.contextName(0)].concat(params)
};
},
@@ -892,11 +915,11 @@ JavaScriptCompiler.prototype = {
}
if (this.trackIds) {
options.ids = "[" + ids.join(",") + "]";
options.ids = this.source.generateArray(ids);
}
if (this.stringParams) {
options.types = "[" + types.join(",") + "]";
options.contexts = "[" + contexts.join(",") + "]";
options.types = this.source.generateArray(types);
options.contexts = this.source.generateArray(contexts);
}
if (this.options.data) {
@@ -907,7 +930,7 @@ JavaScriptCompiler.prototype = {
if (useRegister) {
this.useRegister('options');
params.push('options');
return 'options=' + options;
return ['options=', options];
} else {
params.push(options);
return '';
@@ -915,6 +938,7 @@ JavaScriptCompiler.prototype = {
}
};
var reservedWords = (
"break else new var" +
" case finally return void" +
+2 -1
View File
@@ -21,7 +21,8 @@
"node": ">=0.4.7"
},
"dependencies": {
"optimist": "~0.3"
"optimist": "~0.3",
"source-map": "^0.1.40"
},
"optionalDependencies": {
"uglify-js": "~2.3"
+2
View File
@@ -9,6 +9,8 @@ global.Handlebars = undefined;
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js');
global.CompilerContext = {
browser: true,
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
+1 -1
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
return "";
return "";
},"useData":true});
});
+4
View File
@@ -60,6 +60,10 @@ describe('helpers', function() {
}};
shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
});
it('helper returning undefined value', function() {
shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' ');
shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' ');
});
it("block helper", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
+1 -1
View File
@@ -63,7 +63,7 @@ describe('javascript-compiler api', function() {
});
it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return $superAppend.call(this, string + ' + "_foo"');
return $superAppend.call(this, [string, ' + "_foo"']);
};
shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo");
});
+45
View File
@@ -0,0 +1,45 @@
/*global CompilerContext, Handlebars */
var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
describe('source-map', function() {
if (!Handlebars.precompile) {
return;
}
it('should safely include source map info', function() {
var template = Handlebars.precompile('{{hello}}', {destName: 'dest.js', srcName: 'src.hbs'});
equal(!!template.code, true);
equal(!!template.map, !CompilerContext.browser);
});
it('should map source properly', function() {
var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'});
if (template.map) {
var consumer = new SourceMapConsumer(template.map),
lines = template.code.split('\n'),
srcLines = source.split('\n'),
generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines);
var mapped = consumer.originalPositionFor(generated);
equal(mapped.line, source.line);
equal(mapped.column, source.column);
}
});
});
function grepLine(token, lines) {
for (var i = 0; i < lines.length; i++) {
var column = lines[i].indexOf(token);
if (column >= 0) {
return {
line: i+1,
column: column
};
}
}
}