Incompatibility when bundling code that contains both ESM and CommonJS
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 40.1k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
Note I may just be missing a flag somewhere. If that is the case would love to know about it!
common.cjs
const fs = require('node:fs/promises');
console.log('IN COMMONJS LAND');
(async () => {
await fs.readFile('foo', 'utf8');
await import('./module.mjs');
})()
module.mjs
import { readFile } from 'node:fs/promises';
console.log('IN ESM LAND');
console.log(await readFile('foo', 'utf8'))
Goal:
To create a bundled file that contains both of these files, and is legally executable.
Considering that common.cjs is the entrypoint, my first attempt is:
esbuild common.cjs --bundle --platform=node --format=cjs --outfile=bundle.cjs
This fails on
✘ [ERROR] Top-level await is currently not supported with the "cjs" output format
module.mjs:5:12:
5 │ console.log(await readFile('foo', 'utf8'))
╵ ~~~~~
Ok, I can probably work with that. Since we want to support ESM internally, I switch format to esm.
esbuild common.cjs --bundle --platform=node --format=esm --outfile=bundle.cjs
That successfully creates a build! But the syntax is incorrect.
Note that my output file is a .cjs so I expect commonjs format... Perhaps this conflicts with my --format=esm setting...
bundle.cjs
var __getOwnPropNames = Object.getOwnPropertyNames;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined")
return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// module.mjs
var module_exports = {};
import { readFile } from "node:fs/promises";
var init_module = __esm({
"module.mjs"() {
"use strict";
console.log("IN ESM LAND");
console.log(readFile("foo", "utf8"));
}
});
// common.cjs
var require_common = __commonJS({
"common.cjs"() {
var fs = __require("node:fs/promises");
console.log("IN COMMONJS LAND");
(async () => {
await fs.readFile("foo", "utf8");
await Promise.resolve().then(() => (init_module(), module_exports));
})();
}
});
export default require_common();
Note a couple issues.
The import { readFile } from "node:fs/promises"; is hoisted to the top level!
This is illegal in cjs.
I understand the attempt, and that non-dynamic imports have to be top level, but I would expect the bundler to work around this.
For maximum "compatibility" I would expect something like:
foo.mjs
import { bar } from 'bar';
import bing from 'bing';
to transform into something like
var init_foo = __esm({
async "foo.mjs"() {
const [
{ bar },
{ default: bing }
] = await Promise.all([
import('bar'),
import('bing'),
])
}
});
To maintain the parallel async loading functionality of ESM.
The second issue with this cjs output is the necessity of require to exist.
typeof require !== "undefined"
I would have expected this to be polyfilled (along with values like __dirname) via something like
const require = createModule(importURLToFilePath(import.meta.url));
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the issue with common.cjs and module.mjs using the two esbuild CLI commands shown, then inspect the generated bundle.cjs. Compare the output against the stated goal: a legally executable CommonJS bundle that preserves the ESM module behavior without relying on unsupported syntax or unavailable runtime values.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100