rollup vs esbuild:
iife is not umd

by — posted on

Rollup, Webpack, and new esbuild can bundle JS programs to be usable in webpages, to be added as a <script>. However, there's a problem with esbuild.

It produces opens in a new tab iife builds for browsers, not umd.

You might ask, what's the difference? They both work as intended in browsers, right?

Yes, but you can't import/require esbuild iife bundles and unit test them:

// test.js
import { stripHtml } from "../dist/string-strip-html.dev.umd";

// run unit tests on this stripHtml()

This shortcoming stems from a limited mindset, addressing only "web development" workflow, workflow most of us follow at a day job.

Namely.

Developers write React application, unit-test components through RTL, end-to-end test the web app through cypress, then (let's say) use esbuild to bundle the JS file for distribution to prod. That JS file is pretty much guaranteed to be solid; it does not need any unit testing because RTL/cypress nailed the bugs.

However, there's another scenario, after-work open-source hacking:

A developer writes a program to be open-sourced and published to npm. The source is in cutting-edge ES2099, plus contains all the comments and console.log's so it can't be shipped as-is. Plus, we want a playground for users to test, straight from jsDelivr. We need to bundle, before even unit tests start. Bundler, esbuild or Rollup is used to produce different builds: CommonJS, ES Modules and a build for browsers. Unit tests run off those builds, not the source in ES2099.

Now, if we can't unit test all the builds — programs we produce — there's no way to completely guarantee the quality.

I've just had fixed a hardcore bug in CJS builds, caused by a single misconfigured line in Babel, loose set to true. Rollup built fine, except Babel was casually omitting spread operators here and there, in CJS builds only. All that time, ESM and UMD builds were fine. It just happened that I was assuming CJS will be solid; it should not break, at least without ESM failing too. Wrong.

Furthermore, since I write unit tests anyway, I can easily repurpose unit tests to check all three builds instead of one. All I need to do is to pipe them through an intermediary helper function opens in a new tab, calculate a result for each build, compare them to ensure they all match, then return the result of esm build for further consumption in unit test asserts.

It's easy in tap/ava the unit test runners, although commonly-used Jest does not pass t, the main test instance opens in a new tab so bad news Jest users. Basically, instead of:

test('did not rain', () => {
expect(inchesOfRain(value)).toBe(0);
});

We'd tap the test's instance, t, which in hypothetical Jest case would look like:

test('did not rain', (t) => {
t.expect(inchesOfRain(t, value)).toBe(0);
});

In real tap tests, it would rather resemble:

tap.test('did not rain', (t) => {
t.equal(inchesOfRain(t, value), 0);
});

And inchesOfRain() would add extra asserts, like doing extra calculations against different builds, then assert their differences, then return the result as normal.

Again, this shows how different programming and SPA web development workflows are — even unit test runner principles are different.

§ Takeaway

The esbuild iife builds are not umd opens in a new tab builds, iife builds can't be unit-tested, unlike Rollup's umd. It's a big deal.

Related packages:

📦 esbuild opens in a new tab
An extremely fast JavaScript bundler and minifier
📦 rollup opens in a new tab
Next-generation ES module bundler