-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When used with require()
and node
, does not honor "main" field in package.json if "module" is present
#363
Comments
This could possibly be related: node-fetch/node-fetch#450 (comment) But I couldn't resolve it with just the extensions suggestion alone: $ node -p "$(echo "typeof require('node-fetch')" | npx esbuild --bundle --platform=node --resolve-extensions=.js)"
object |
Hmmm. It may be that esbuild is failing to resolve the $ grep main node_modules/node-fetch/package.json
"main": "lib/index",
$ node -p "$(echo "typeof require('node-fetch/lib/index')" | npx esbuild --bundle --platform=node --resolve-extensions=.js)"
function In the latest node-fetch beta, they've fixed any ambiguity around file endings, so But esbuild has the same problem with failing to resolve $ npm install node-fetch@3.0.0-beta.8
$ node -p "$(echo "typeof require('node-fetch')" | npx esbuild --bundle --platform=node)"
object
$ node -p "$(echo "typeof require('node-fetch/dist/index.cjs')" | npx esbuild --bundle --platform=node)"
function |
I just checked Parcel and Webpack and they also do the same thing. The problem is that modern bundlers prefer Edit: I see that is the same issue that node-fetch/node-fetch#450 (comment) is talking about. The solution is mentioned in the thread below that comment: use |
The linked comment suggested that for Node.js, it should use “main”, because that’s what Node.js uses. If we’re explicitly bundling for node, shouldn’t esbuild do the same thing?
…Sent from my iPhone
On Sep 3, 2020, at 2:43 AM, Evan Wallace ***@***.***> wrote:
I just checked Parcel and Webpack and they also do the same thing. The problem is that modern bundlers prefer module over main because that leads to better tree shaking. It doesn't have to do with file extensions. The code require('node-fetch') pulls in the file in the module field, and that file contains export default async function fetch() {}. This means the require() call returns an object with the fetch function as the default property, since that's how modules work.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
Also, wouldn't that mean that any module in node_modules that's using fetch also won't bundle correctly? Users don't always have control over the code doing the require. Is there a reason that you wouldn't use the |
require()
and node
, does not honor "main" field in package.json if "module" is present
Updated title to reflect what I suspect is the underlying issue here (which you hinted at: esbuild always prefers |
FWIW, I know you used Webpack and Parcel as examples here, but they also just happen to have the same bug (with Webpack you can configure it, not sure about Parcel). Browserify and Rollup get it right out of the box. Related: |
Doing what you suggested (picking The current JavaScript module situation is a mess honestly. It wasn't designed holistically and there's no behavior that works the best in all cases. The design of the Some bundlers do have a setting to prefer I'm not sure what the right solution is here. It doesn't seem like other bundlers have come up with a good fix either. Maybe fine-grained per-package "main field" ordering? Maybe the right solution is just for the |
Why don't you think Browserify and Rollup have a good fix? They seem to be doing the right thing AFAICT |
I just checked Rollup and it also seems to result in an object instead of a function just like Parcel and Webpack. Note that to get Rollup to bundle anything, you need to configure it to use Browserify does result in a function but it looks like it maybe it just doesn't support ES modules at all? I'm not sure because I'm not familiar with that tool myself, but that would explain why it's a function instead of an object in Browserify. Here are the commands I'm using: https://gist.github.com/evanw/a8d65017c7876e85861245af7821d038. Run I'm not sure what your exact situation is but if you're ok with the resulting bundle still depending on the |
I don't get that: $ npm install node-fetch
$ npx rollup -c rollup.config.js entry.js -o out-rollup.js
$ node out-rollup.js
function Browserify doesn't support ES modules – it's by far one of the oldest bundlers I know and has always been CommonJS AFAIK. This issue is about using I think the question is, does esbuild want to support Node.js' resolution algorithm or not? |
That's interesting. I wonder why we're getting different results with Rollup. The code that Rollup generates looks like this for me: var lib = /*#__PURE__*/Object.freeze({
__proto__: null,
'default': fetch,
Headers: Headers,
Request: Request,
Response: Response,
FetchError: FetchError
});
console.log(typeof lib); What does the relevant code that Rollup generates look like for you? Here's a link to an online version that generates the same output.
My goal is to improve esbuild's compatibility both with real-world code and with other bundlers. I do want to make this case work but the fact that other bundlers match esbuild's current behavior gives me pause. Although I can see the argument for having esbuild behave the way you describe since node also behaves that way. You will also get duplicate modules in memory in node if you use both
Running |
But why would I use I'm just using require – and I want esbuild to build it just like webpack and browserify and rollup do (yes, some of those need specific settings to prefer In terms of your rollup output I'm not sure what's going on – you sure you're using the current version of node-fetch? |
Node is adding native support for Even if you don't use both in your code, my concern is that this could still happen when libraries you use do this in their code. So then you end up with multiple copies: the CommonJS version from your code and the ECMAScript module version from the library code.
I tried both the current version (2.6.1) and the beta version mentioned above (3.0.0-beta.8) and they both resulted in an object when bundled with Rollup. You can see the version information for all of the packages involved here: https://repl.it/@EvanWallace/LinenSwiftAlgorithm#package.json. |
Again, I just don't understand how this would happen if you're just using |
You're right on rollup – I completely apologize – it requires configuration. Here's what actually happened: $ npx rollup -c rollup.config.js entry.js -o out-rollup.js --silent
npx: installed 2 in 0.982s
$ node out-rollup.js
internal/modules/cjs/loader.js:968
throw err;
^
Error: Cannot find module 'node-fetch'
$ npm install node-fetch
+ node-fetch@2.6.1
added 1 package from 1 contributor and audited 56 packages in 0.764s
$ node out-rollup.js
function Forgot to recompile 🤦 It works for import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import builtins from 'builtin-modules';
export default {
output: { exports: 'auto', format: 'cjs' },
plugins: [
nodeResolve({ mainFields: ['main'] }),
commonjs({ ignore: builtins }),
],
}; For import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import builtins from 'builtin-modules';
export default {
output: { exports: 'auto', format: 'cjs' },
plugins: [
nodeResolve({ mainFields: ['main'], extensions: ['.js'] }),
commonjs({ ignore: [...builtins, 'encoding'] }),
],
}; So in summary:
|
For completeness, works with $ npx @vercel/ncc run entry.js
npx: installed 1 in 3.066s
ncc: Version 0.24.0
ncc: Compiling file index.js
122kB sourcemap-register.js
375kB index.js
190kB index.js.map
497kB [913ms] - ncc 0.24.0
function |
Ok I'm planning to give fixing this a shot. This is potentially a breaking change so I'm planning to make this when esbuild hits 0.7.0. |
Awesome – even if it's just an option allowing the precedence of |
I'm pretty sure Node.js completely ignores the However, given the prevalence of both webpack and To be clear, in any case, I would definitely not expect the resolved file (i.e. |
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 |
Node.js defines a clear mechanism for creating packages that conditionally export different files for {
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
} I would absolutely expect esbuild to respect this, just like Node.js does. However, this is entirely unrelated to Regarding module duplication when usage of Broadly speaking, I think there are essentially two overwhelmingly dominant module resolution algorithms used in the JS ecosystem:
I'd fear that adding a third option would merely add further complexity to the already hopelessly complicated module resolution situation. |
Keep in mind that this issue is about I think I largely agree with what you're saying. However, I don't think Part of the design of esbuild is to try to automatically "do the right thing" without too much configuration. All of the little configuration options can add up to quite a bit of overhead. Right now esbuild is pretty simple to configure and use and I'd like to keep that property if possible. If there's a solution that is automatic without configuration and doesn't have significant downsides, then I'm inclined to take that approach.
The node algorithm doesn't appeal to me because you can end up with duplicates of the same module in memory. I understand that it can be worked around but avoiding it entirely seems preferable if possible.
I don't like the idea of the I like the solution I outlined above because it seems to have the best properties of both options. It should automatically handle |
Is the |
I'm not sure material-ui is a good example here – all the guidance and tutorials seem to use ES modules. This issue is more for plain-old-Node.js apps using purely require/CommonJS |
Why is it important to have per-package granularity? If a package is broken by either of the following schemes, it isn't compatible with the vast majority of the ecosystem and is IMHO probably not worth consideration.
This is what Node.js, Parcel, ncc, and Next.js do by default. This is the most "correct", but I assume the ability for esbuild to tree shake is harmed. However, is tree-shaking really that crucial for server-side bundles? This seems like a perfectly acceptable outcome to me.
This is what Webpack and Rollup (via Selecting just one option seems adequate, but also having a binary flag between the two would not be a large maintenance burden and conforms to both widely-used methods. On the other hand, co-opting an existing package.json field (albeit a loosely-defined one) with new semantics runs the risk of having a pernicious influence on the ecosystem. As a user, it is hard enough already to try and understand how bundlers and other tools resolve dependencies. Is yet another way really needed? I remain puzzled by the notion that If there truly is a package that needs special treatment, I think there are plenty of viable workarounds that need not involve a novel module resolution algorithm:
This seems in line with how esbuild handles other non-standard features. I understand there is sometimes tradeoff between correctness and usability, but in this case, I think it is really worth considering the potential risks of a new module resolution scheme. Sorry for the long-winded response. You should obviously do what you feel is best for your project (I am merely hoping to persuade you to consider alternatives 😄). |
according to #81 (comment): This switches over from ES6 to CommonJS in UserMenu.js which does a nested import from @material-ui/icons/AccountCircle.js instead of just importing AccountCircle from @material-ui/icons. That bypasses the "module": "./esm/index.js" field @material-ui/icons/package.json and the CommonJS version is picked instead of the ES6 version. |
That doesn't preclude it from still not being a good example – as I said, all the guidance there is written under the assumption that if you're using this on Node.js, you're using a bundler. See https://material-ui.com/guides/server-rendering/ . It's a frontend library written with ES in mind – not a plain-old-Node.js app. |
I was under the impression that this situation is not specific to |
I'd prefer this issue not get sidetracked with ES module concerns. It's specifically about environments where only Think about it: you're writing a standard (pre-v14) Node.js app that works with no bundler. All the code that you write uses |
If there's some clever way to determine that an ES module could be loaded instead of a CommonJS one, and it provides a noticeable benefit, then obviously that's fine – but I think most people would prefer correctness over bundle size – at least for their Node.js apps |
The proposal in #363 (comment) would work in that scenario as well. What's the downside? |
It's totally fine by me – only reason I suggested it be configurable was in case there were ppl relying on the existing behavior |
Wow this is definitely the most contentious esbuild issue so far :) I think I'm going to try to go with my proposal. It would automatically fix this issue ( I also like this approach because it doesn't involve adding any configuration options, so it's easier to roll back and try something else later if it becomes clear later that there's a better solution. |
I actually ended up kind of doing both options. By default esbuild will now prefer the |
Amazing! 🙌 Thanks so much for taking the time to talk it through. Has worked well so far in a bunch of scenarios I've tested. |
The only case I've had to manage manually is modules with native bindings. Hard to say if this is really the responsibility of a bundler TBH Eg, const bignum = require('bignum')
const b = bignum('782910138827292261791972728324982') Bundling and running: $ ~/github/esbuild/esbuild --bundle --platform=node --outfile=test.bundle.js test.js
node_modules/bindings/bindings.js:94:8: warning: Indirect calls to "require" will not be bundled
: require;
~~~~~~~
1 warning
$ node test.bundle.js
/tmp/test/test.bundle.js:116
throw err;
^
Error: Could not locate the bindings file. Tried:
→ /tmp/test/build/bignum.node
→ /tmp/test/build/Debug/bignum.node
→ /tmp/test/build/Release/bignum.node
→ /tmp/test/out/Debug/bignum.node
→ /tmp/test/Debug/bignum.node
→ /tmp/test/out/Release/bignum.node
→ /tmp/test/Release/bignum.node
→ /tmp/test/build/default/bignum.node
→ /tmp/test/compiled/12.18.3/darwin/x64/bignum.node
→ /tmp/test/addon-build/release/install-root/bignum.node
→ /tmp/test/addon-build/debug/install-root/bignum.node
→ /tmp/test/addon-build/default/install-root/bignum.node
→ /tmp/test/lib/binding/node-v72-darwin-x64/bignum.node
at bindings (/tmp/test/test.bundle.js:112:11)
at /tmp/test/test.bundle.js:223:31
at /tmp/test/test.bundle.js:4:5
at Object.<anonymous> (/tmp/test/test.bundle.js:569:16)
at Module._compile (internal/modules/cjs/loader.js:1137:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)
at Module.load (internal/modules/cjs/loader.js:985:32)
at Function.Module._load (internal/modules/cjs/loader.js:878:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47 {
tries: [
'/tmp/test/build/bignum.node',
'/tmp/test/build/Debug/bignum.node',
'/tmp/test/build/Release/bignum.node',
'/tmp/test/out/Debug/bignum.node',
'/tmp/test/Debug/bignum.node',
'/tmp/test/out/Release/bignum.node',
'/tmp/test/Release/bignum.node',
'/tmp/test/build/default/bignum.node',
'/tmp/test/compiled/12.18.3/darwin/x64/bignum.node',
'/tmp/test/addon-build/release/install-root/bignum.node',
'/tmp/test/addon-build/debug/install-root/bignum.node',
'/tmp/test/addon-build/default/install-root/bignum.node',
'/tmp/test/lib/binding/node-v72-darwin-x64/bignum.node'
]
}
$ find . -name '*.node'
./node_modules/bignum/build/Release/bignum.node So (in this case at least), it's up to the user to manually relocate the I think this is totally acceptable – but just in case users post issues with this error, this is the reason. FWIW, |
(or of course, declare all modules with native bindings as external, and provide a |
When switching our bundling mechanism to esbuild, we've hit this exact issue with My understanding of what's happening is the following:
Changing The only thing I can think of is to write a plugin that targets const replacerPlugin = {
name: "node-fetch-handler",
setup(build) {
build.onResolve({ filter: /^node-fetch$/ }, ({ kind, path }) => {
if (kind !== "require-call") {
return;
}
return {
path: require.resolve(path),
};
});
},
}; Does this feel like a reasonable approach? Thanks in advance! |
That's not necessarily true. I don't think node supports implicit
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 |
In this case, would you consider changing the default value of
If someone uses the module with an ES Modules |
I'm actually considering just removing them entirely. Given that they can break things, they should probably be opt-in instead of opt-out. I'm also considering forbidding importing an ESM file from a CommonJS file in an upcoming version of esbuild for certain reasons (e.g. confusing import order, incompatible with top-level await) and accidentally resolving to a
Some libraries aren't designed to have two copies of themselves in the bundle. So even if you're ok with bloating your bundle like that, including a library twice can cause correctness issues in addition to bloat. |
For anyone landing here for the workaround for node-fetch with esbuild, place a patch-package diff at --- a/node_modules/node-fetch/package.json
+++ b/node_modules/node-fetch/package.json
@@ -2,7 +2,7 @@
"name": "node-fetch",
"version": "2.6.1",
"description": "A light-weight module that brings window.fetch to node.js",
- "main": "lib/index",
+ "main": "lib/index.js",
"browser": "./browser.js",
"module": "lib/index.mjs",
"files": [ |
This fixes it: |
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 inpackage.json
if you're usingrequire()
withnode
. 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:
esbuild/internal/resolver/resolver.go
Lines 272 to 280 in 99f587a
But this means that certain packages just don't work with
require()
, such asnode-fetch
:To reproduce:
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?
The text was updated successfully, but these errors were encountered: