How to import() a JavaScript String

You can use arbitrary JavaScript in front matter in Eleventy project files (via the js
type).
Historically Eleventy has made use of the node-retrieve-globals
package to accomplish this, which was a nightmarish conglomeration of a few different Node.js approaches (each with different advantages and drawbacks).
Related research: Dynamic Script Evaluation in JavaScript
The biggest drawbacks to node-retrieve-globals
include:
- CommonJS code only (even in a
require(esm)
world). While dynamicimport()
works,import
andexport
do not. Top levelawait
is emulated typically by wrapping your code with anasync
function wrapper. - It uses Node.js only approaches not viable as Eleventy works to deliver a library that is browser-friendly.
Regardless, this abomination was a necessary evil due to the experimental status of Node.js’ vm.Module
(since Node v12, ~2019), the ESM-friendly partner to CommonJS-compatible vm.Script
. I’d still love to see vm.Module
achieve a level of stability, but I digress.
New Best Friend is import()
Moving forward, I’ve been having success from a much lighter approach using import()
, described in Evaluating JavaScript code via import() by Dr. Axel Rauschmayer. It looks something like this:
let code = `export default function() {}`;
let u = `data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`;
let mod = await import(u);
Newer runtimes with Blob
support might look like this (example from David Bushell):
let code = `export default function() {}`;
let blob = new Blob([code], {type: "text/javascript"});
let u = URL.createObjectURL(blob);
let mod = await import(u);
URL.revokeObjectURL(u);
Limitations
- Importing a Blob of code does not work in Node.js (as of v24), despite Node having support for Blob in v18 and newer.
Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'blob:'
import.meta.url
just points to the parentdata:
orblob:
, which isn’t super helpful in script.- No import of relative references, even if you’ve mapped it to a full path via an Import Map.
- e.g.
import './relative.js'
TypeError: Failed to resolve module specifier ./relative.js: Invalid relative url or base scheme isn't hierarchical.
- e.g.
- No import of bare references. These can be remapped via Import Maps.
- e.g.
import 'barename'
TypeError: Failed to resolve module specifier "barename". Relative references must start with either "/", "./", or "../".
- e.g.
Though interestingly, Node.js will let you import builtins e.g. import 'node:fs'
.
Enter import-module-string
I’ve worked around the above limitations and packaged this code up into import-module-string
, a package that could be described as a super lightweight runtime-independent (server or client) JavaScript bundler.
I was able to repurpose a package I created in June 2022 to help here: esm-import-transformer
recursively preprocesses and transform imports to remap them to Blob
URLs (falling back to data:
when a feature test determines Blob doesn’t work).
import { importFromString } from "import-module-string";
await importFromString(`import num from "./relative.js";
export const c = num;`);
Where relative.js
contains export default 3;
, the above code becomes (example from Node.js):
await importFromString(`import num from "data:text/javascript;charset=utf-8,export%20default%203%3B";
export const c = num;`);
Which returns:
{ c: 3 }
This transformation happens recursively for all imports (even imports in imports) with very little ceremony.
When you’ve added a <script type="importmap">
Import Map to your HTML, the script will use import.meta.resolve
to use the Import Map when resolving module targets.
Even more features
A few more features for this new package:
- Extremely limited dependency footprint, only 3 dependencies total:
acorn
,acorn-walk
, andesm-import-transformer
. - Multi-runtime: tested with Node (18+), some limited testing in Deno, Chromium, Firefox, and WebKit.
- This was my first time using Vitest and it worked pretty well! I only hit one snag trying to test
import.meta.resolve
.
- This was my first time using Vitest and it worked pretty well! I only hit one snag trying to test
- Supports top-level
async
/await
(as expected in ES modules) - If you use
export
, the package uses your exports to determine what it returns. If there is noexport
in play, it implicitly exports all globals (viavar
,let
,const
,function
,Array
orObject
destructuring assignment,import
specifiers, etc), emulating the behavior innode-retrieve-globals
. You can disable implicit exports usingimplicitExports: false
. - Emulates
import.meta.url
when thefilePath
option is supplied addRequire
option adds support forrequire()
(this feature is exclusive to server runtimes)- Supports a
data
object to pass in your own global variables to the script. These must beJSON.stringify
friendly, though this restriction could be relaxed with more serialization options later. - When running in-browser, each script is subject to URL content size maximums: Chrome
512MB
, Safari2048MB
, Firefox512MB
, Firefox prior to v13732MB
.
As always with dynamic script execution, do not use this mechanism to run code that is untrusted (especially when running in-browser on a domain with privileged access to secure information like authentication tokens). Make sure you sandbox appropriately!