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 eval
ed (or in
this case, new Function
ed) 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, eval
ed 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!)