Zach’s ugly mug (his face) Zach Leatherman

How to import() a JavaScript String

A large container ship runs aground
June 09, 2025

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 dynamic import() works, import and export do not. Top level await is emulated typically by wrapping your code with an async 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

  1. 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:'

  2. import.meta.url just points to the parent data: or blob:, which isn’t super helpful in script.
  3. 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.

  4. 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 "../".

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, and esm-import-transformer.
  • Multi-runtime: tested with Node (18+), some limited testing in Deno, Chromium, Firefox, and WebKit.
  • 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 no export in play, it implicitly exports all globals (via var, let, const, function, Array or Object destructuring assignment, import specifiers, etc), emulating the behavior in node-retrieve-globals. You can disable implicit exports using implicitExports: false.
  • Emulates import.meta.url when the filePath option is supplied
  • addRequire option adds support for require() (this feature is exclusive to server runtimes)
  • Supports a data object to pass in your own global variables to the script. These must be JSON.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, Safari 2048MB, Firefox 512MB, Firefox prior to v137 32MB.

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!


Older >
line-numbers Web Component

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at Font Awesome and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 85 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)