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,importandexportdo not. Top levelawaitis emulated typically by wrapping your code with anasyncfunction 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.urljust points to the parent- data:or- blob:, 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 noexportin play, it implicitly exports all globals (viavar,let,const,function,ArrayorObjectdestructuring assignment,importspecifiers, etc), emulating the behavior innode-retrieve-globals. You can disable implicit exports usingimplicitExports: false.
- Emulates import.meta.urlwhen thefilePathoption is supplied
- addRequireoption adds support for- require()(this feature is exclusive to server runtimes)
- Supports a dataobject to pass in your own global variables to the script. These must beJSON.stringifyfriendly, 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!



















4 Comments
Craig Patik
Is this safer than eval() ?
Zach Leatherman
if by safer you mean as a security mechanism, no!
Nicolò Ribaudo
Why did you choose to inline dependency imports as data URLs, rather than replacing relative specifiers with absolute specifiers?
Zach Leatherman :11ty:
@khalidabuhakmeh just a small feature support wrinkle in Vitest, happy to see it ironed out!