esbuild: When used with `require()` and `node`, does not honor "main" field in package.json if "module" is present
EDIT: Originally ran into this with node-fetch, but it seems to have surfaced a larger issue which is that esbuild doesn’t prioritize the main
field in package.json
if you’re using require()
with node
. This means that esbuild diverges from how Node.js/CommonJS would require the same package.
It appears from the comments that this is by design:
We support ES6 so we always prefer the “module” field over the “main” field.
But this means that certain packages just don’t work with require()
, such as node-fetch
:
To reproduce:
$ node --version
v12.18.3
$ npm install node-fetch
$ node -p "typeof require('node-fetch')"
function
$ node -p "$(echo "typeof require('node-fetch')" | npx esbuild --bundle --platform=node)"
object
Would be great if esbuild supported the same behavior as Node.js for require’ing modules – if not out-of-the-box, then possibly via a flag?
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 44 (19 by maintainers)
Commits related to this issue
- add main fields and prefer "main" for node (#363) — committed to evanw/esbuild by evanw 4 years ago
When switching our bundling mechanism to esbuild, we’ve hit this exact issue with
node-fetch
. We have users calling this module asrequire('node-fetch')
, which breaks when switching to esbuild because the return value is an object, not a function.My understanding of what’s happening is the following:
main
export is ambiguous, sincelib/index
may resolve tolib/index.js
orlib/index.mjs
, depending on which extension the consumer prefers;.mjs
over.js
, which effectively makes themain
export point to an ESM file;default
property as opposed to the expected value, a function.Changing
resolveExtensions
to prefer.js
over.mjs
solves the issue, but it doesn’t feel like the right default behaviour. I’m hoping to find a solution that allows existing code to work without compromising better-compliant modules.The only thing I can think of is to write a plugin that targets
require
calls tonode-fetch
and forces the resolved path to belib/index.js
, leaving aloneimport
calls so that ES Modules use the correct entry point:Does this feel like a reasonable approach?
Thanks in advance!
After doing some research, it appears that a lot of people do expect this. As a counterpoint, here is an example of what kind of problems doing so causes in practice. I think this thread was a good summary of the issue and the opinions of the people behind different bundlers.
One approach I’m currently considering is for
require
to read from"main"
andimport
to read from"module"
, but later on during linking when the whole module graph is available, esbuild could rewrite"module"
to"main"
for any module that is used with bothrequire
andimport
. That should “just work” automatically without any configuration while also not causing problems with duplicate modules in the bundle. One drawback is that esbuild may spend extra time parsing duplicate copies of a module only to throw out that work later, but that seems like an acceptable trade-off to me for being able to handle this case automatically without any configuration.That’s not necessarily true. I don’t think node supports implicit
.mjs
extensions so you could argue that preferring.js
would be more compatible when targeting node.If you’re trying to customize the behavior for an individual package, then you will likely need to use a plugin.
Personally I would recommend unconditionally redirecting all imports to
lib/index.js
regardless of how they were imported. I don’t think there’s any reason to have two copies of the same in module your bundle.I was under the impression that this situation is not specific to
--platform=node
and could occur anytime the same module is required/imported by different code leading to two copies of the library in memory - commonjs and esm. Anyway, material-ui aside, the proposal in the last paragraph in https://github.com/evanw/esbuild/issues/363#issuecomment-689265736 seems to be a reasonable solution to avoid duplication.