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
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.
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.
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.
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?
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).
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.)
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.
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.