<title>How to import() a JavaScript String</title>
<link href="https://www.zachleat.com/web/dynamic-import/"/>
<updated>2025-06-09T05:00:00Z</updated>
<id>https://www.zachleat.com/web/dynamic-import/</id>
<content type="html"><p>You can use arbitrary <a href="https://www.11ty.dev/docs/data-frontmatter/#java-script-front-matter">JavaScript in front matter in Eleventy project files</a> (via the <code>js</code> type).</p>
<p>Historically Eleventy has made use of the <code>node-retrieve-globals</code> package to accomplish this, which was a nightmarish conglomeration of a few different Node.js approaches (each with different advantages and drawbacks).</p>
<p><em>Related research: <a href="https://github.com/zachleat/javascript-eval-modules">Dynamic Script Evaluation in JavaScript</a></em></p>
<p>The biggest drawbacks to <code>node-retrieve-globals</code> include:</p>
<ul>
<li>CommonJS code only (even in <a href="https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/">a <code>require(esm)</code> world</a>). While dynamic <code>import()</code> works, <code>import</code> and <code>export</code> do not. Top level <code>await</code> is emulated typically by wrapping your code with an <code>async</code> function wrapper.</li>
<li>It uses Node.js only approaches not viable as Eleventy works to deliver <a href="https://fediverse.zachleat.com/@zachleat/114434795493653605">a library</a> that is <a href="https://neighborhood.11ty.dev/@11ty/114519676689929120">browser-friendly</a>.</li>
</ul>
<p>Regardless, this abomination was a necessary evil due to the experimental status of Node.js’ <a href="https://nodejs.org/docs/latest/api/vm.html#class-vmmodule"><code>vm.Module</code></a> (since Node v12, ~2019), the ESM-friendly partner to CommonJS-compatible <code>vm.Script</code>. I’d still love to see <code>vm.Module</code> achieve a level of stability, but I digress.</p>
<h2 id="new-best-friend-is-import">New Best Friend is <code>import()</code></h2>
<p>Moving forward, I’ve been having success from a much lighter approach using <code>import()</code>, described in <a href="https://2ality.com/2019/10/eval-via-import.html"><em>Evaluating JavaScript code via import()</em> by Dr. Axel Rauschmayer</a>. It looks something like this:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> code <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">export default function() {}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">let</span> u <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">data:text/javascript;charset=utf-8,</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>code<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">let</span> mod <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span>u<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Newer runtimes with <code>Blob</code> support might look like this (<a href="https://github.com/dbushell/dinossr/blob/f555a4231c230aebc563194fc88778eb58270879/src/bundle/import.ts#L13-L16">example from David Bushell</a>):</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">let</span> code <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">export default function() {}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">let</span> blob <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Blob</span><span class="token punctuation">(</span><span class="token punctuation">[</span>code<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"text/javascript"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> u <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> mod <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">import</span><span class="token punctuation">(</span>u<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">revokeObjectURL</span><span class="token punctuation">(</span>u<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h3 id="limitations">Limitations</h3>
<ol>
<li>Importing a Blob of code does <em>not</em> work in Node.js (as of v24), despite Node having support for Blob in v18 and newer.
<blockquote>
<p>Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'blob:'</p>
</blockquote>
</li>
<li><code>import.meta.url</code> just points to the parent <code>data:</code> or <code>blob:</code>, which isn’t super helpful in script.</li>
<li>No import of relative references, even if you’ve mapped it to a full path via an Import Map.
<ul>
<li>e.g. <code>import './relative.js'</code>
<blockquote>
<p>TypeError: Failed to resolve module specifier ./relative.js: Invalid relative url or base scheme isn't hierarchical.</p>
</blockquote>
</li>
</ul>
</li>
<li>No import of bare references. These <em>can</em> be remapped via Import Maps.
<ul>
<li>e.g. <code>import 'barename'</code>
<blockquote>
<p>TypeError: Failed to resolve module specifier "barename". Relative references must start with either "/", "./", or "../".</p>
</blockquote>
</li>
</ul>
</li>
</ol>
<p>Though interestingly, Node.js <em>will</em> let you import builtins e.g. <code>import 'node:fs'</code>.</p>
<h2 id="enter-import-module-string">Enter <code>import-module-string</code></h2>
<p>I’ve worked around the above limitations and packaged this code up into <code>import-module-string</code>, a package that <em>could</em> be described as a super lightweight runtime-independent (server or client) JavaScript bundler.</p>
<ul>
<li><a href="https://github.com/zachleat/import-module-string"><code>import-module-string</code> on GitHub</a></li>
<li><a href="https://www.npmjs.com/package/import-module-string"><code>import-module-string</code> on npm</a></li>
</ul>
<p>I was able to repurpose <a href="https://www.zachleat.com/web/esm-import-transformer/">a package I created in June 2022</a> to help here: <a href="https://github.com/zachleat/esm-import-transformer"><code>esm-import-transformer</code></a> recursively preprocesses and transform imports to remap them to <code>Blob</code> URLs (falling back to <code>data:</code> when a feature test determines Blob doesn’t work).</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> importFromString <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"import-module-string"</span><span class="token punctuation">;</span>
<span class="token keyword">await</span> <span class="token function">importFromString</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">import num from "./relative.js";
export const c = num;</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Where <code>relative.js</code> contains <code>export default 3;</code>, the above code becomes (example from Node.js):</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">await</span> <span class="token function">importFromString</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">import num from "data:text/javascript;charset=utf-8,export%20default%203%3B";
export const c = num;</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Which returns:</p>
<pre class="language-js"><code class="language-js"><span class="token punctuation">{</span> <span class="token literal-property property">c</span><span class="token operator">:</span> <span class="token number">3</span> <span class="token punctuation">}</span></code></pre>
<p>This transformation happens recursively for all imports (even imports in imports) with very little ceremony.</p>
<p>When you’ve added a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap"><code><script type="importmap"></code> Import Map</a> to your HTML, the script will use <code>import.meta.resolve</code> to use the Import Map when resolving module targets.</p>
<h3 id="even-more-features">Even more features</h3>
<p>A few more features for this new package:</p>
<ul>
<li>Extremely limited dependency footprint, only 3 dependencies total: <code>acorn</code>, <code>acorn-walk</code>, and <code>esm-import-transformer</code>.</li>
<li>Multi-runtime: tested with Node (18+), some limited testing in Deno, Chromium, Firefox, and WebKit.
<ul>
<li>This was my first time using <a href="https://vitest.dev/">Vitest</a> and it worked pretty well! I only hit one snag trying to <a href="https://github.com/vitest-dev/vitest/issues/6953">test <code>import.meta.resolve</code></a>.</li>
</ul>
</li>
<li>Supports top-level <code>async</code>/<code>await</code> (as expected in ES modules)</li>
<li>If you use <code>export</code>, the package uses your exports to determine what it returns. If there is no <code>export</code> in play, it implicitly exports all globals (via <code>var</code>, <code>let</code>, <code>const</code>, <code>function</code>, <code>Array</code> or <code>Object</code> destructuring assignment, <code>import</code> specifiers, etc), emulating the behavior in <code>node-retrieve-globals</code>. You can disable implicit exports using <code>implicitExports: false</code>.</li>
<li>Emulates <code>import.meta.url</code> when the <code>filePath</code> option is supplied</li>
<li><code>addRequire</code> option adds support for <code>require()</code> (this feature is exclusive to server runtimes)</li>
<li>Supports a <code>data</code> object to pass in your own global variables to the script. These must be <code>JSON.stringify</code> friendly, though this restriction could be relaxed with more serialization options later.</li>
<li>When running in-browser, each script is subject to URL content size maximums: Chrome <code>512MB</code>, Safari <code>2048MB</code>, Firefox <code>512MB</code>, Firefox prior to v137 <code>32MB</code>.</li>
</ul>
<p>As always with dynamic script execution, do not use this mechanism to run code that is untrusted (<em>especially</em> when running in-browser on a domain with privileged access to secure information like authentication tokens). Make sure you sandbox appropriately!</p>
</content>