import()
The import()
operator lets us dynamically load ECMAScript modules. But they can also be used to evaluate JavaScript code (as Andrea Giammarchi recently pointed out to me), as an alternative to eval()
. This blog post explains how that works.
eval()
does not support export
and import
A significant limitation of eval()
is that it doesn’t support module syntax such as export
and import
.
If we use import()
instead of eval()
, we can actually evaluate module code, as we will see later in this blog post.
In the future, we may get Realms which are, roughly, a more powerful eval()
with support for modules.
import()
Let’s start by evaluating a console.log()
via import()
:
const js = `console.log('Hello everyone!');`;
const encodedJs = encodeURIComponent(js);
const dataUri = 'data:text/javascript;charset=utf-8,'
+ encodedJs;
import(dataUri);
// Output:
// 'Hello everyone!'
What is going on here?
data:
. The remainder of the URI encodes the full resource instead pointing to it. In this case, the data URI contains a complete ECMAScript module – whose content type is text/javascript
.Warning: This code only works in web browsers. On Node.js, import()
does not support data URIs.
The fulfillment value of the Promise returned by import()
is a module namespace object. That gives us access to the default export and the named exports of the module. In the following example, we access the default export:
const js = `export default 'Returned value'`;
const dataUri = 'data:text/javascript;charset=utf-8,'
+ encodeURIComponent(js);
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
});
With an appropriate function esm
(whose implementation we’ll see later), we can rewrite the previous example and create the data URI via a tagged template:
const dataUri = esm`export default 'Returned value'`;
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
});
The implementation of esm
looks as follows:
function esm(templateStrings, ...substitutions) {
let js = templateStrings.raw[0];
for (let i=0; i<substitutions.length; i++) {
js += substitutions[i] + templateStrings.raw[i+1];
}
return 'data:text/javascript;base64,' + btoa(js);
}
For the encoding, we have switched from charset=utf-8
to base64
. Compare:
'a' < 'b'
data:text/javascript;charset=utf-8,'a'%20%3C%20'b'
data:text/javascript;base64,J2EnIDwgJ2In
Each of the two ways of encoding has different pros and cons:
charset=utf-8
(percent-encoding):
base64
:
btoa()
is a global utility function that encodes a string via base 64. Caveats:
With tagged templates, we can nest data URIs and encode a module m2
that imports another module m1
:
const m1 = esm`export function f() { return 'Hello!' }`;
const m2 = esm`import {f} from '${m1}'; export default f()+f();`;
import(m2)
.then(ns => assert.equal(ns.default, 'Hello!Hello!'));