Zach’s ugly mug (his face) Zach Leat­herman

Lessons learned moving Eleventy from CommonJS to ESM

Watch in 45 minutes 1k 00 February 07, 2024

This talk was given at TheJam.dev 2024.

Watch on YouTube: Lessons learned moving Eleventy from CommonJS to ESM

And the full slide deck is included below:

11ty v3
CommonJS
Common, the actor and rapper is shown alongside the JS logo
CommonJS is strikethrough, ESM is added
Regina George from Mean Girls is shown riding in a lunar module and says Get in Loser—We’re using Modules now
module.exports = function() {};
export default function() {};
const render = function() {}; module.exports = { render };
const render = function() {}; export { render };
A screenshot of the Eleventy core test suite passing on GitHub actions (1048 tests) on Mac/Linux/Windows
Ryan Reynolds in medical scrubs asks Why?
EMCAScript modules are the official standard format to package JavaScript code for reuse. (via TC39 and the node.js documentation)
A screenshot of the web browser support for EMCAScript 2015 (all green)
A table showing CommonJS support in Node but missing in Deno and the Browser but ESM supported in all.
Bundlers! Rollup, esbuild, Carabiner, Parcel, Webpack, SWC
Actually sorry Carabiner is just the Toblerone logo
A repeat of the CommonJS/ESM support in Node, Deno, Browser
Minimum viable tech stack means we stay as close to the Build/no-build line as possible, avoiding as many dependencies as we can to stick as close to HTML/CSS/JS as we can.
A fake messages conversation about web sites being easier to maintain because they do less!
Eleventy v1 was 72.7MB of node_modules, v2 was 35.2MB
Eleventy v3 is currently 20.2MB of node_modules
For comparison, rollup is 4.8MB, webpack is 25.9MB, parcel is 281.9MB, esbuild is 9.9MB and swc is 47MB.
Gatsby is 523.8MB
Eleventy v3 is dropping some node_modules weight through our community survey results. Handlebars, Pug, Mustache, EJS, and Haml are all unpopular template syntaxes and we’ll move these into plugin-land.
Requirements
Node.js 10 ESM was experimental but it’s stable in 12
11ty v1 required Node.js 12+, v2 required 14+, v3 requires 18+
We could have done this in 2022, but we didn’t 😅
Compatibility
CommonJS Project compatibility, config files, data files, template files, third party plugins.
The biggest limitation with a CommonJS project is that you can’t require an ESM dependency (which Eleventy is, now)
Compatibility overview.
CommonJS Upgrade Walkthrough in Two Steps
Step one—npm install @11ty/eleventy@canary --save-exact
You cannot require(
Change your config callback to async and use await import(
Done.
Now let’s talk about Third-party plugins
Existing third-party plugins are CommonJS—keep these as-is.
eleventyConfig.addPlugin(async function() {}) now supported in Eleventy v3
Any new ESM Eleventy Plugins are only compatible with Eleventy v3
ESM Eleventy plugins require an `async` config callback or an ESM configuration file—both of which are only supported in Eleventy v3
Our compatibility checklist for CommonJS projects is complete.
Next, ESM application code.
A similar checklist of config files, data files, templates, and plugins.
ESM Upgrade Walkthrough in Three Steps
Step one npm install @11ty/eleventy@canary --save-exact, Step two add type: 'module' to your package.json. All .js files in your project are now assumed to be ESM.
Step three rename all .js app files to .cjs (and convert these over to ESM as-needed or not at all)
Done.
Let’s convert our configuration code to ESM
Use import {} from
Change data files from module.exports = to export.default
You can used named exports too. export { key }
export default { data, render } in an index.11ty.js template.
export { data, render } in an index.11ty.js template.
__filename and __dirname aren’t available by default but you can use import.meta.url and fileURLToPath to recreate these.
Eventually we’ll use import.meta.filename and import.meta.dirname (not yet stable as of Node 21)
Let’s talk about Third-party plugins in an ESM project.
Everything will work as-is, you can import any ESM or CJS plugin from an ESM config file. Access to all the things!
The full checklist of compatibility is complete.
Node Scripts, Eleventy’s Programmatic API
Again, you can’t require(
Dynamic await import(
If your script is ESM, you can `import Eleventy from
Internals
Upgrade frozen CommonJS dependencies (these went full ESM), @sindresorhus/slugify, multimatch, bcp-47-normalize
Reimporting without require.cache to get new content
Before we would delete from require.cache and re-require the file. Now we await import with a cache buster URL parameter.
Parsing the dependency tree. Before this was available via require.cache. Now we parse the JS with acorn (but it only happens for --serve/--watch)
Import attributes, before we could require a JSON file directly. You can’t await import a JSON file now.
You could import a JSON file: assert { type: 'json' } with a warning
The syntax changed to: with { type: 'json' } also with a warning
We just use fs.readFile* to avoid warnings.
Dynamic execution of JavaScript for `---node` front matter.
Node’s vm module is only stable for CommonJS code. Fine until you try to `import` something.
You can use node --experimental-vm-modules but we fake it by transforming `import` to dynamic `import()` until this is stable.
Conclusions
ESM ~~all~~ of the things
ESM some of the things.
It’s better to be an ESM dependent.
It’s better to be a CommonJS dependency.
ESM for me, CommonJS for thee (fancy Winnie the Pooh meme)
Titus Wormer’s chart on the popularity of ESM/CJS across popular npm packages. November 2023 had 68.8% CJS, 12.9% Faux, 7.5% Dual, 10.8% ESM
11ty and Astro are ESM first—Svelte, Vite, Nuxt are dual—Remix and Gatsby are faux—Next.js is CJS.
For me, I will not be dual publishing packages. I don’t want the overhead.
Looking at package compatibility—if things are going to include you, it’s tempting to use CommonJS . If you’re going to use other things, it’s better to be ESM.
But we have to think of the wider ecosystem outside of Node.js (other runtimes and the Browser).
The Parasite movie poster.
The Parasite movie poster overlaid with Parasites vs. Para-apps.
Thanks! (the end)

Correction: on Slide 40 the code reads eleventyPlugin.addPlugin when it should say eleventyConfig.addPlugin.


< Older
carouscroll Web Component
Newer >
throbber Web Component

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at Font Awesome and the creator of Build Awesome (née IndieWeb Avatar for https://www.11ty.devEleventy/11ty), an award-winning open source website generator. He measures website performance with speedlify and at one point became too fixated on web fonts. He has given 89 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)