dynamic `esmExport()` to allow concatting multiple esm and non-esm libraries into single rollup.

motivation:

  1. optimize websites like jslint.com which uses both esm and non-esm libraries by allowing it to concat them all into a single file:
// non-esm website-rollup.js - start
(async function () {


// concat esm jslint.mjs
await (async function () {
...
// dynamically export as either esm or cjs
if (typeof esmExport === "function") {
   esmExport("jslint", Object.freeze(jslint));
} else if (typeof module === "object" && module?.exports) {
    module.exports.jslint = Object.freeze(jslint);
}
}());


// concat esm foo.mjs
await (async function () {
let bar = await import("./bar.mjs");
...
if (typeof esmExport === "function") {
   esmExport("foo", Object.freeze(foo));
} else if (typeof module === "object" && module?.exports) {
    module.exports.foo = Object.freeze(foo);
}
}());

// concat non-esm codemirror.js
(function () {
(function (global, factory) {
...
globalThis.CodeMirror = factory();
}());


}());
// non-esm website-rollup.js - end
  1. jslint.com currently has to maintain 2 versions of jslint: jslint.cjs and jslint.mjs.
    dynamic esmExport() will remove this maintennance burden allowing a single file jslint.js to be readable by both esm and cjs module-loaders.

It seems much simpler to use a build process with ESM and CJS as inputs, and generate a purely CJS bundle as output.

It seems much simpler to use a build process with ESM and CJS as inputs, and generate a purely CJS bundle as output.

that's exactly what this proposal is trying to get rid of -- excessive tooling-burden that makes simple web-projects more expensive than they need be.

Concatenation is far worse than bundling, because JS files aren't meant to be naively concatenated. If you're using any build process (concatenation or minification included) then the burden of using a bundler is a sunk cost, but the advantages of doing so are large.

iow, if the goal is to avoid tooling, then concatenation is off the table as a motivating use case.

2 Likes

Concatenation is far worse than bundling, because JS files aren't meant to be naively concatenated

false - jslint.mjs for example, is meant to be "almost" naively concatenatable -- "almost" because the only thing preventing it is the static export statement, which this proposal is trying to workaround.

The language itself does not intend that, so the intentions of the authors imo are irrelevant in the face of that.

I'm not sure I follow. You're trying to simplify the build process, by providing better support for using a concatenation tool instead of a tool like Webpack? Webpack is just a concatenator, with a few extra bells and whistles, some configuration, and some "smarts" to avoid bugs that home-brew concatenators often have, like scoping issues, properly applying "use strict" to the correct region, correctly preventing ASI hazards when first and last lines of modules touch, etc.

Why build your own build step script, or encourage people to build their own, when a good, polished build-step solution already exists that avoids all sorts of bugs? Couldn't this jslint.com website just swap out its custom build tool for something that's able to handle these kinds of things and call it a day?

Claiming a bundler is just a concatenator with extra bells and whistles is like saying dialysis is just leeches with extra bells and whistles.

Haha, alright, it might not be fair to put it on the same plain as concatenators.

But yeah, point is, why use a concatenators when you can use a bundler?

because there are weirdos like me who don't want to use bundlers and tooling we don't completely understand in web-projects if it can be helped.

but lets move on to reason #2. people complain all the time about having to maintain separate .cjs / .mjs variants for code they ship. dynamic esmExport() solves the problem allowing you ship a single isomorphic file importable by both cjs and mjs loaders, e.g.

// foo.js
// this is a self-contained, isomophic module
// that can be loaded as cjs, native-esm, or globally-attached.
// no need to burden yourself with maintaining separate foo.cjs / foo.mjs copies
(function () {
    function foo(...) {
        ...
    }
    // dynamically export foo as native esm.
    if (typeof esmExport === "function") {
        esmExport("foo", Object.freeze(foo));
    // else export as cjs.
    } else if (typeof module === "object" && module.?exports) {
        module.exports.foo = Object.freeze(foo);
    // else globally attach foo (in legacy browsers).
    } else {
        globalThis.foo = foo;
    }
}());
#!/bin/sh

# isomorphic foo.js can be loaded as native esm
node --input-type=module -e '
import {foo} from "./foo.js";
...
'

# isomorphic foo.js can also be loaded as cjs
# (no need to maintain separate foo.cjs variant)
node -e '
let foo = require("./foo.js").foo;
...
'

Unless you're a library maintainer or you're using a library that requires it, you don't have to use it. I only add a bundler (usually Rollup) if it's likely to be complex enough to merit one, and mostly-static sites almost never merit it in practice. And even in cases where many first start reaching for some form of organization, I can usually punt the scaling problem pretty far using Mithril (which is small enough and flexible enough many of us in that community use it as a jQuery replacement).

Not sure where you hear that complaint from - Node deals with .js just fine as long as you add a "type": "module" to your package.json, and ES modules can also import CommonJS modules as applicable. And I don't think I've once ever even seen a .cjs file in the wild - the use case is somewhat niche outside large app migration.

Keep in mind, this is coming from someone who has for years maintained a pure CommonJS library targeting primarily browsers - I can assure you that Node's resolution mechanism is not a remotely common source of integration issues. (That's all in the land of bundlers - I've had to not only report a detailed bug with minimal repro to Webpack because they were failing to resolve a certain module correctly, but I've also have had to revert a change in a major version update release candidate because of by-design bundler resolution differences.)

1 Like

Alright - it's a tool, and it's there to be used for this specific purpose. I can understand not wanting to use extra tools when your project isn't mature enough to need them, it keeps the project simpler, but when it comes time to start thinking about these kinds of things, I would definitely choose the tool that's specifically built to handle this job well. Can't complain that a pocket knife doesn't do very good at chopping wood when there's an axe sitting next to you ;).

But, you were wanting to focus on your second point anyways.

I'm just going to build on what @claudiameadows said with specific examples. You'll find these examples to be very similar to what you already showed.

// this is a self-contained, isomophic module
// that can be loaded as cjs, native-esm, or globally-attached.
// no need to burden yourself with maintaining separate foo.cjs / foo.mjs copies
export.foo = function (...) {
    ...
}
#!/bin/sh

# isomorphic foo.js can be loaded as native esm
node --input-type=module -e '
import module from "./foo.js";
...
'

# isomorphic foo.js can also be loaded as cjs
# (no need to maintain separate foo.cjs variant)
node -e '
let foo = require("./foo.js").foo;
...
'

# And if you really want to do so...
node --input-type=module -e '
import module from "./foo.js";
globalThis.foo = module.foo
...
'

In this scenario, we're creating a library that would require a form of bundler (because we're using cjs, which isn't understood by browsers), so we can just use one of the lightweight ones that @claudiameadows suggested instead of reaching for a concatenator script.

And @kaizhu256, just to add on to what @theScottyJam said, it's not hard to set up your own module loader in a concatenative environment as well if you really wanted to. It's not black magic, I promise.

Oh! Didn't know that was possible - good to know :). So I guess using commonjs libraries don't require bundlers.

Look at the repo. It's not a CommonJS loader. :wink:

Ah, I looked at it too quickly. I saw this line "Require "module-name" CommonJS-style." and assumed it meant it was requiring a commonjs module, not that the require function was similar (but not the same) as commonjs.

1 Like

Yeah, it's just a sales pitch. (I like to include those in my libraries sometimes.) :slightly_smiling_face: