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:
mainexport is ambiguous, sincelib/indexmay resolve tolib/index.jsorlib/index.mjs, depending on which extension the consumer prefers;.mjsover.js, which effectively makes themainexport point to an ESM file;defaultproperty as opposed to the expected value, a function.Changing
resolveExtensionsto prefer.jsover.mjssolves 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
requirecalls tonode-fetchand forces the resolved path to belib/index.js, leaving aloneimportcalls 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
requireto read from"main"andimportto 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 bothrequireandimport. 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
.mjsextensions so you could argue that preferring.jswould 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.jsregardless 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=nodeand 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.