TL;DR
When using esbuild
to bundle code with --platform=node that depends on npm
packages with a mixture of cjs
and esm
entry points, use the following rule of thumb:
- When using --bundle, set --format to
cjs
. This will work in all cases except foresm
modules with top-level await.-
--format=esm
can be used but requires a polyfill such as this one.
-
- When using --packages=external, set
--format
toesm
.
If you're wondering about the difference between cjs
and esm
, take a look at Node.js: A brief history of cjs, bundlers, and esm.
Symptom
When executing esbuild bundled code with --platform=node
you may have come across one of the following runtime errors:
Error: Dynamic require of "<module_name>" is not supported
Error [ERR_REQUIRE_ESM]: require() of ES Module (...) from (...) not supported.
Instead change the require of (...) in (...) to a dynamic import() which is available in all CommonJS modules.
Cause
This is because of one of the following limitations:
-
esbuild
'sesm
tocjs
(and vice-versa) transformations. - Node.js
cjs
/esm
interoperability.
Analysis
esbuild
has limited transformation capabilities between esm
and cjs
. Additionally, some scenarios while supported by esbuild
are not supported by Node.js itself. As of esbuild@0.24.0, the following table summarizes what's supported:
Format | Scenario | Supported? |
---|---|---|
cjs |
static import
|
Yes |
cjs |
dynamic import()
|
Yes |
cjs |
top-level await
|
No |
cjs |
--packages=external of esm entry point |
No* |
esm |
require() of user modules** |
Yes*** |
esm |
require() of node:* modules |
No**** |
esm |
--packages=external of cjs entry point |
Yes |
* Supported by esbuild
but not by Node.js
** Refers to npm
packages or relative path files.
*** User modules are supported with some caveats: __dirname and __filename are not supported without a polyfill.
**** node:*
modules can be supported with the same polyfill.
What follows is a detailed description of these scenarios without the use of any polyfills:
npm packages
We'll use the following example npm
packages:
static-import
esm
module with a static import
:
import { version } from "node:process";
export function getVersion() {
return version;
}
dynamic-import
esm
module with a dynamic import()
within an async function
:
export async function getVersion() {
const { version } = await import("node:process");
return version;
}
top-level-await
esm
module with a dynamic import()
and a top-level await
:
const { version } = await import("node:process");
export function getVersion() {
return version;
}
require
cjs
module with a require()
invocation:
const { version } = require("node:process");
exports.getVersion = function() {
return version;
}
--format=cjs
We'll run esbuild
with the following arguments:
esbuild --bundle --format=cjs --platform=node --outfile=bundle.cjs src/main.js
and the following code:
import { getVersion } from "{npm-package}";
(async () => {
// version can be `string` or `Promise<string>`
const version = await getVersion();
console.log(version);
})();
static import
Produces the following which runs just fine:
// node_modules/static-import/index.js
var import_node_process = require("node:process");
function getVersion() {
return import_node_process.version;
}
// src/main.js
(async () => {
const version2 = await getVersion();
console.log(version2);
})();
dynamic import()
Produces the following which runs just fine:
// (...esbuild auto-generated helpers...)
// node_modules/dynamic-import/index.js
async function getVersion() {
const { version } = await import("node:process");
return version;
}
// src/main.js
(async () => {
const version = await getVersion();
console.log(version);
})();
Notice how the dynamic import()
is not transformed to a require()
because it's also allowed in cjs
modules.
top-level await
esbuild
fails with the following error:
[ERROR] Top-level await is currently not supported with the "cjs" output format
node_modules/top-level-await/index.js:1:20:
1 │ const { version } = await import("node:process");
╵ ~~~~~
--packages=external
Using --packages=external
succeeds with all npm
packages:
esbuild --packages=external --format=cjs --platform=node --outfile=bundle.cjs src/main.js
produces:
var npm_package_import = require("{npm-package}");
(async () => {
const version = await (0, npm_package_import.getVersion)();
console.log(version);
})();
However, they all fail to run because Nodes.js doesn't allow cjs
modules to import esm
modules:
/(...)/bundle.cjs:1
var import_static_import = require("static-import");
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /(...)/node_modules/static-import/index.js from /(...)/bundle.cjs not supported.
Instead change the require of index.js in /(...)/bundle.cjs to a dynamic import() which is available in all CommonJS modules.
--format=esm
We'll now run esbuild
with the following arguments:
esbuild --bundle --format=esm --platform=node --outfile=bundle.mjs src/main.js
require() of user modules
src/main.js
const { getVersion } = require("static-import");
console.log(getVersion());
produces the following which runs just fine:
// (...esbuild auto-generated helpers...)
// node_modules/static-import/index.js
var static_import_exports = {};
__export(static_import_exports, {
getVersion: () => getVersion
});
import { version } from "node:process";
function getVersion() {
return version;
}
var init_static_import = __esm({
"node_modules/static-import/index.js"() {
}
});
// src/main.js
var { getVersion: getVersion2 } = (init_static_import(), __toCommonJS(static_import_exports));
console.log(getVersion2());
require() of node:* modules
src/main.js
import { getVersion } from "require";
console.log(getVersion());
produces the following:
// (...esbuild auto-generated helpers...)
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');
});
// (...esbuild auto-generated helpers...)
// node_modules/require/index.js
var require_require = __commonJS({
"node_modules/require/index.js"(exports) {
var { version } = __require("node:process");
exports.getVersion = function() {
return version;
};
}
});
// src/main.js
var import_require = __toESM(require_require());
console.log((0, import_require.getVersion)());
However, it fails to run:
Error: Dynamic require of "node:process" is not supported
--packages=external
Using --packages=external
succeeds with all npm
packages, including those with cjs
entry points. For example:
esbuild --packages=external --format=esm --platform=node --outfile=bundle.mjs src/main.js
with:
src/index.js
import { getVersion } from "require";
console.log(getVersion());
produces a nearly-verbatim output which runs just fine because esm
modules can import npm
packages with cjs
entry points:
import { getVersion } from "require";
console.log(getVersion());
Conclusion
I hope you find this post useful to troubleshoot esbuild
outputs now and in the future. Let me know your thoughts below!
Top comments (0)