A minimalist development workflow using ES modules and esinstall

When a new-ish web developer wants to start building a frontend JavaScript application these days, they typically reach for a project-starting tool—like Create React App or Parcel—to set up their codebase. This works and gets you running quickly, but it’s at the cost of massive tooling complexity abstracted beneath the surface (if you don’t believe me, check your node_modules directory)!

Fortunately, we can bypass this complexity by skipping the bundler and using ECMAScript modules in our apps directly (browser support for modules is good now). This is a big deal! As DHH recently said, “An entire class of complexity stands at the precipice.” The only problem is that lots of 3rd-party JavaScript packages continue to only export to CommonJS, making them incompatible with ES module apps.

In this post, I’ll share a how we can use esinstall to pull in any 3rd-party package as an ES module, thus unlocking a minimalist, bundle-free development workflow for modern JavaScript apps.

Setup: HTML, CSS, and JavaScript

To get set up, we just need a basic HTML file that pulls in main.js as our JavaScript entrypoint:

<!DOCTYPE html>
<html lang="en">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Project</title>
  <h1>Hello World</h1>
  <script type="module" src="main.js"></script>

By declaring type="module" on our script tag, we’re safe to use import and export statements in main.js without needing a bundler. 💪

Pulling in dependencies

To pull in dependencies, we’ll create a basic package.json and list them under dependencies like normal. Here’s an example, pulling in a dom-confetti package:

  "name": "esinstall-demo",
  "dependencies": {
    "dom-confetti": "^0.2.2"

dom-confetti only exports to CommonJS, so we’re not able to import it into our app yet. This is where esinstall comes in.


esinstall is a development tool that converts any JavaScript package into a single ES6-module-friendly file that you can check into your codebase and import. You can run this conversion just once after install, so there’s no need to set up file watching or other unnecessary tooling.

To set it up, we’ll update our package.json like so:

  "name": "esinstall-demo",
  "scripts": {
    "build": "node install-modules.mjs"
  "dependencies": {
    "dom-confetti": "^0.2.2"
  "devDependencies": {
    "esinstall": "^1.1.7"
  "esinstall": {
    "install": [

This includes a few important changes:

  • We declared esinstall as a dev-dependency, so we can use it for our one-time builds.
  • We set up a new “esinstall” section, for listing any packages we want to install as ES modules.
  • We created a build task for running the one-time build.

This build task runs a small node script that I’ve created called install-modules.mjs. All this script does is gets the list of modules from our package.json and runs them through esinstall:

// install-modules.mjs

import { install } from 'esinstall';
import { readFile } from 'fs/promises';

const json = JSON.parse(
  await readFile(
    new URL('./package.json', import.meta.url)
const moduleList = json.esinstall.install;
const installOptions = json.esinstall.installOptions;

await install(moduleList, installOptions);

Now all we need to do is run npm install && npm run build and our dom-confetti package gets downloaded, converted to an ES Module, and dropped into a web_modules folder, ready for us to import:

// main.js

import { confetti } from "./web_modules/dom-confetti.js";

confetti(document.body.firstElementChild, {
  angle: "60",
  duration: "6000",
  stagger: "30"

a confetti burst on an HTML page

Is that all we need?

Pretty much! Now you can organize everything in your codebase as modules and ship them straight to the browser bundle-free. If you have questions about how modules handle things like caching, performance, or browser support, feel free to check out my earlier post, ES modules in production: my experience so far.

Some final thoughts

One interesting thing about esinstall is that it was born out of the Snowpack bundler. Snowpack’s big idea was to embrace cross-browser support for ES modules to speed up and simplify development. While Snowpack is no longer actively maintained, the Snowpack team decided to take its core functionality for converting code to ES modules, publish it as its own package, and name it esinstall.

Hopefully, our industry will get to a place where all 3rd-party JavaScript packages provide an ESM-formatted export, ready to use in in the browser. Until then, tools like esinstall can help bridge the gap without introducing unnecessary complexity.