Naming `eval` Scripts with the `//# sourceURL` Directive
In Firefox 36, SpiderMonkey (Firefox's JavaScript engine) now supports the
//# sourceURL=my-display-url.js directive. This allows developers to give
a name to a script created by eval or new Function, and get better stack
traces.
To demonstrate this, let's use a minimal version of
John Resig's micro templater. The
micro templater compiles template strings into JS source code that it passes to
new Function, thus transforming the template string into a function.
function tmpl(str) {
return new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');");
};
The details of how the template is converted into JavaScript source code isn't
of import; what is important is that it dynamically creates new scripts via code
evaluated in new Function.
We can define a new templater function:
var hello = tmpl("<h1>Hello, <%=name%></h1>");
And use it like this:
hello({ name: "World!" });
// "<h1>Hello, World!</h1>"
When we get an error, SpiderMonkey will generate a name for the evaled (or in
this case, new Functioned) script based on the location where the call to
eval (or new Function) occurred. For our concrete example, this is the
generated name for the hello templater function's frame:
file:///Users/fitzgen/scratch/foo.js line 2 > Function
And here it is in the context of an error with the whole stack trace:
hello({ name: Object.create(null) });
// TypeError: can't convert Object to string
// Stack trace:
// anonymous@file:///Users/fitzgen/scratch/foo.js line 2 > Function:1:107
// @file:///Users/fitzgen/scratch/foo.js:28:3
Despite being a solid improvement over just "eval frame" or something of that
sort, these stack traces can still be difficult to read. If there are many
different templater functions, the value of the eval script's introduction
location is further diminished. It is difficult to determine which of the many
functions created by tmpl contains the thrown error, because they all end up
with the same name, because they were all created at the same location.
We can improve this situation with the //# sourceURL directive.
Consider this version of the tmpl function adapted to use the //# sourceURL
directive:
function tmpl(name, str) {
return new Function("obj",
"var p=[],print=function(){p.push.apply(p,arguments);};" +
// Introduce the data as local variables using with(){}
"with(obj){p.push('" +
// Convert the template into pure JavaScript
str
.replace(/[\r\t\n]/g, " ")
.split("<%").join("\t")
.replace(/((^|%>)[^\t]*)'/g, "$1\r")
.replace(/\t=(.*?)%>/g, "',$1,'")
.split("\t").join("');")
.split("%>").join("p.push('")
.split("\r").join("\\'")
+ "');}return p.join('');"
+ "//# sourceURL=" + name);
};
Note that the function takes a new parameter called name and appends //#
sourceURL=<name> to the end of the generated JS code passed to new Function.
With this new version of tmpl, we create our templater function like this:
var hello = tmpl("hello.template", "<h1>Hello <%=name%></h1>");
Now SpiderMonkey will use the name given by the //# sourceURL directive,
instead of using a name based on the introduction site of the script:
hello({ name: Object.create(null) });
// TypeError: can't convert Object to string
// Stack trace:
// anonymous@hello.template:1:107
// @file:///Users/fitzgen/scratch/foo.js:25:3
Giving the eval script a name makes it easier for us to debug errors originating from it, and we can give a different name to different scripts created at the same location.
The //# sourceURL directive is also particularly useful for dynamic code- and
module-loaders, which fetch source text over the network and then eval it.
Additionally, in Firefox 37, evaled sources with a //#
sourceURL directive will be debuggable and labeled with the name specified by
the directive in the debugger. (Update: //# sourceURL support in the
debugger is also available in Firefox 36!)