ES2015 in Dev Tools console without extensions

How to load and use ES2015 in the Chrome DevTools console without any 3rd party extensions.

I showed how to load any JavaScript library on almost any website when you open DevTools console. Now let us go one step further and use ES6/ES2015 directly in DevTools console.

Loading Babel

To compile ES6 to ES5 we can use a 3rd party library. Babel.js is a full featured ES2015 to ES5 transpiler available from a CDN. One can easily add a code snippet to load babel.js whenever needed

1
2
3
4
5
(function () {
var s = document.createElement('script');
s.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser.js');
document.body.appendChild(s);
}());

I recommend saving the above code as a code snippet and running when you need BabelJS. Now we can execute small ES6 code fragments from the console

1
2
eval(babel('`es6 here`').code) // or eval(babel.transform('`es6 here`').code)
//=> "es6 here"

Goal

I like loading babel.js on demand, but I hate writing eval(babel(...).code) around ES6 source. Can we transpile any entered source using Babel.js by default and then evaluate it? Chrome DevTools console already evaluates template string literals

1
2
var foo = 42; `foo is ${foo}`
//=> "foo is 42"

Other ES6 features are not evaluated yet, for example let and const are unavailable by default

1
2
3
4
5
6
let foo = 'foo'
VM643:2 Uncaught SyntaxError: Block-scoped declarations (let, const, function, class)
not yet supported outside strict mode
at Object.InjectedScript._evaluateOn (<anonymous>:905:140)
at Object.InjectedScript._evaluateAndWrap (<anonymous>:838:34)
at Object.InjectedScript.evaluate (<anonymous>:694:21)

Most ES6 features do not work yet. For example, the arrow functions are not working even in the strict mode yet

[1, 2, 3].map(x => x*x)
VM937:2 Uncaught SyntaxError: missing ) after argument list

But they work just fine via Babel

eval(babel('[1, 2, 3].map(x => x*x)').code)
[1, 4, 9]

Inspect the error

Let us look at the additional information when we try to evaluate an unsupported ES6 source in the console. Click the mouse on the small arrow next to the exception information to show additional 3 lines (the "InjectedScript" lines in the below snippet)

[1, 2, 3].map(x => x*x)
VM937:2 Uncaught SyntaxError: missing ) after argument list
    at Object.InjectedScript._evaluateOn (<anonymous>:905:140)
    at Object.InjectedScript._evaluateAndWrap (<anonymous>:838:34)
    at Object.InjectedScript.evaluate (<anonymous>:694:21)
    InjectedScript._evaluateOn @ VM141:905
    InjectedScript._evaluateAndWrap @ VM141:838
    InjectedScript.evaluate @ VM141:694

Notice that the text fragments "VM141" in this case are links. You can click on them to get into the "virtual machine" code that actually evaluates the JavaScript code entered by the user in the console.

The top level method where the exception happens is "InjectedScript._evaluateOn". Here is its signature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
+ @param {?JavaScriptCallFrame} callFrame
+ @param {string} objectGroup
+ @param {string} expression
+ @param {boolean} injectCommandLineAPI
+ @param {!Array.<!Object>=} scopeChain
+ @return {*}
*/
_evaluateOn: function(callFrame, objectGroup, expression, injectCommandLineAPI, scopeChain)
{
// wrap the expression by adding command line object (console)
// evaluate the wrapped source
// clean up scope
}

In this case, the expression is whatever we have entered in the console, for example [1, 2, 3].map(x => x*x). Other arguments are

callFrame: null
objectGroup: "console"
expression: "[1, 2, 3].map(x => x*x)"
injectCommandLineAPI: true
scopeChain: undefined

Interestingly, inside the _evaluateOn method, the entered expression gets wrapped with additional code to give the code access to a few extras

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_evaluateOn: function(callFrame, objectGroup, expression, injectCommandLineAPI, scopeChain)
{
...
var prefix = "";
var suffix = "";
if (injectCommandLineAPI) {
InjectedScriptHost.setNonEnumProperty(inspectedWindow, "__commandLineAPI", new CommandLineAPI(this._commandLineAPIImpl, callFrame));
prefix = "with (typeof __commandLineAPI !== 'undefined' ? __commandLineAPI : { __proto__: null }) {";
suffix = "}";
}
// evaluate the wrapped source
if (prefix)
expression = prefix + "\n" + expression + "\n" + suffix;
var wrappedResult = callFrame ?
callFrame.evaluateWithExceptionDetails(expression, scopeExtensionForEval) :
InjectedScriptHost.evaluateWithExceptionDetails(expression);
// clean up scope
}

The window.__commandLineAPI object has Chrome DevTools specific properties, like table, profile and $0 for example. We can set a break in the _evaluateOn method to inspect it

$0: (...)
$$: $$(selector, [startNode])
$_: Array[3]
$x: $x(xpath, [startNode])
copy: copy(object)
debug: debug(fn)
...

The wrapper code uses with (__commandLineAPI) { ...} to give your expression full access to $0, copy and other methods without needing to use the full syntax window.__commandLineAPI.copy. Interestingly, these methods are not the same as the console methods with the same name

window.__commandLineAPI.table === console.table
//=> false

Because of with keyword and different implementations, your own variant of console.table() takes precedence over table().

After wrapping the expression with prefix and suffix, the source that the JavaScript interpretor is going to evaluate is

"with (typeof __commandLineAPI !== 'undefined' ? __commandLineAPI : { __proto__: null }) {
eval('[1, 2, 3].map(x => x*x)')
}"

Hmm, this text is then passed to the InjectedScriptHost to be evaluated, because callFrame is always null when evaluating code from the console.

1
2
3
4
5
6
// evaluate the wrapped source
if (prefix)
expression = prefix + "\n" + expression + "\n" + suffix;
var wrappedResult = callFrame ?
callFrame.evaluateWithExceptionDetails(expression, scopeExtensionForEval) :
InjectedScriptHost.evaluateWithExceptionDetails(expression);

What would be great is to overwrite the InjectedScriptHost.evaluateWithExceptionDetails and replace it with something like this

1
2
3
4
var _evaluate = InjectedScriptHost.evaluateWithExceptionDetails.bind(InjectedScriptHost);
InjectedScriptHost.evaluateWithExceptionDetails = function (s) {
return _evaluate(babel(s).code);
};

Unfortunately, this is impossible. We do not have direct access to InjectedScriptHost object, it is native to the browser and made available via a closure argument, making it impossible to tamper with.

But ...

We can direct evaluation to a stray method

If we put a break point at the line with the ternary condition

1
2
3
var wrappedResult = callFrame ? 
callFrame.evaluateWithExceptionDetails(expression, scopeExtensionForEval) :
InjectedScriptHost.evaluateWithExceptionDetails(expression);

then we can create a fake callFrame object and it will evaluate the expression!

  • Put a break point at var wrappedResult = callFrame ? ... line, (VM line 905 in Chrome 44.0.2403.130)
  • Type something in the console, for example 'foo'
  • When execution stops at the line, paste the following from the console line
1
2
3
4
5
6
7
callFrame = { 
evaluateWithExceptionDetails: function (s) {
return InjectedScriptHost.evaluateWithExceptionDetails(
babel.transform(s, { blacklist: ['strict'] }).code
);
}
};

Then let the execution continue (for example using 'F8' key).

The execution now transpiles the entered 'foo' expression before evaluating it! We need { blacklist: ['strict'] } BabelJS option to avoid adding use strict to the code, because the expression is wrapped with with () { ... } block, not allowed in strict mode.

Overwrite callFrame semi-automatically

We had to stop the execution and paste code to create fake callFrame object to transpile the code before evaluating. Can we do this automatically? Almost.

Create a new "watch expression" in the DevTools. Paste the above callFrame = { ... } code into the watch expression. Now if you have a breakpoint at line var wrappedResult = callFrame ? ... every time you enter code in the console, it will stop and set the evaluation to transpiled code. If you continue after the breakpoint, then it will correctly evaluate ES6 source.

Pretty cool, right?

It would be even better if we had a dedicated browser API and could set a default transpiler / list of plugins that would get the source expression entered from the console and transform it before evaluation. I do see that as a huge security risk though.