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 | (function () { |
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 | eval(babel('`es6 here`').code) // or eval(babel.transform('`es6 here`').code) |
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 | var foo = 42; `foo is ${foo}` |
Other ES6 features are not evaluated yet, for example let
and const
are unavailable by default
1 | let foo = 'foo' |
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 | /** |
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 | _evaluateOn: function(callFrame, objectGroup, expression, injectCommandLineAPI, scopeChain) |
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 | // evaluate the wrapped source |
What would be great is to overwrite the InjectedScriptHost.evaluateWithExceptionDetails
and
replace it with something like this
1 | var _evaluate = InjectedScriptHost.evaluateWithExceptionDetails.bind(InjectedScriptHost); |
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 | var wrappedResult = callFrame ? |
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 | callFrame = { |
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.