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
chore(cjs/esm): Bundle module and use package exports #1227
Conversation
- use esbuild to bundle module in esm and cjs format - bundle dependencies to reduce module size - use package exports to automatically select the appropriate version - add build step to prepublishOnly - add build step to ci
I added a similar PR on If and when merged, we could avoid bundling diff --git a/package.json b/package.json
index 7f4726d..668aecf 100644
--- a/package.json
+++ b/package.json
@@ -21,8 +21,8 @@
},
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
- "build:esm": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.mjs",
- "build:cjs": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs",
+ "build:esm": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.mjs --external:fetch-blob",
+ "build:cjs": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:fetch-blob",
"test": "mocha",
"coverage": "c8 report --reporter=text-lcov | coveralls",
"test-types": "tsd",
@@ -66,7 +66,6 @@
"data-uri-to-buffer": "^3.0.1",
"delay": "^5.0.0",
"esbuild": "^0.12.16",
- "fetch-blob": "^3.1.2",
"form-data": "^4.0.0",
"formdata-node": "^3.5.4",
"mocha": "^8.3.2",
@@ -74,6 +73,9 @@
"tsd": "^0.14.0",
"xo": "^0.39.1"
},
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
"tsd": {
"cwd": "@types",
"compilerOptions": { |
It feels very unappealing to basically double the hole codebase and start depending on a bundler just to compile everything to cjs and going back. using a module wrapper isn't a good solution either as other dependencies are esm only packages. Many modules steers away from cjs nowdays. It's also quite unappealing to have a own version of whatwg:streams (coming from fetch-blob if we bundled everything and make it dependency free) as different version of whatwg:streams are not interchangeable with other versions. MattiasBuelens/web-streams-polyfill#82 (comment) web streams polyfill could be used by other consumers as well. I would rather suggest a very basic solution that goes something like: // something.cjs
exports = fetch = (...args) => import('node-fetch').then(module => module.default(...args)) You won't be able to import Request, Response or Headers this way but not everyone needs those. If you wish to use node-fetch@v3 then you should be using ESM as well... all new code shouldIt is time that we steer away from using it, cjs won't go away unless we stop using it. |
I think that publishing both CJS and ESM in the package is the wrong way to go. If it's super important to still support the old CommonJS way of importing the module, even though all currently supported Node.js versions supports ESM, I would rather see that we only published as CommonJS. While there is a small upside in bundling the code before publishing to Npm, I think that we should also consider the downsides. E.g. one can no longer install a specific branch or commit by doing
With this change we need to bump and publish a new package any time
Why can't you just do
This is easily solved by using My opinion is that we should stick with ESM only without a bundler |
Just a bit of curious, why do you think it's a wrong way?
Agree but not agree. As you can not write CJS and ESM at the same time.
So, as I understand why dev drop CJS. I might on my own. To decrease maintainance, I choose esbuild. But, I still want to say, please re-consider. Support both as we are still not there yet. |
@loynoir i feel strongly against bundeling whatwg streams and having a own instance (of everything basically) that are not interchangeable just b/c of this reason alone: Do you have any arguments against that? |
I don't see any benefit of doing this, over just publishing a CommonJS module, since ESM can import CommonJS modules. If you know of any benefits I would be happy to hear them! The downside is that you are shipping your entire module twice, which means (apart from doubling the size) that there is a risk that you'll load two different but very similar versions of the package at runtime. This can then lead to the "dual package hazard" which can cause runtime breakage. e.g. https://github.com/GeoffreyBooth/dual-package-hazard
I don't really understand this 🤔 Node.js supports importing ESM modules natively, you do not need neither a bundler or a transpiler?
The stable 2.x line is still fully functional and this package is still in beta. I think it's reasonable that people upgrade to 3.x at the same time when they make sure that they are running a supported versions of Node.js and switches to ESM. |
Hi, @jimmywarting.
I do not want to argue with anyone, as I said I totally understand the movement. And I'm also a big fan of ESM.
Agree. But...😂 $ esbuild --format=cjs --outdir=dist/cjs ./src/*js ./src/errors/*js ./src/utils/*js
dist
└── cjs
├── body.js
├── errors
│ ├── abort-error.js
│ ├── base.js
│ └── fetch-error.js
├── headers.js
├── index.js
├── request.js
├── response.js
└── utils
├── form-data.js
├── get-search.js
├── is.js
└── is-redirect.js > require('./dist/cjs')
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: fetch-blob/index.js Just lack of energy to find out all ESM only dependencies, and fork every of them. 😂 I don't think bundler is a good idea, but at least it's acceptable and quick workaround, for me. Hi, @LinusU
If I rememeber correctly, the last time I check So, maybe it's not native enough. Hi, @SomaticIT Maybe let esbuild do no bundeling, esbuild --format=cjs --outdir=dist/cjs ./src/*js ./src/errors/*js ./src/utils/*js After > require('./dist/cjs')
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: fetch-blob/index.js |
Well…I’d be happy to stick to 2.x if you’d backport some of the features in 3.x I’m using. You‘re effectively making the same argument here about maintaining multiple codebases if you intend to keep 2.x and 3.x (once it comes out of beta) going, no? Also - I think one of the scenarios that’s being missed is situations like mine: some of my code that uses node-fetch are plugins to other projects that don’t yet support ESM. You’re forcing me to version lock in a situation I have little control over, as I’m operating in a broader ecosystem that’s slower to implement ESM support. Now my choices are:
We all recognize it’s beta, and stuff happens and major breaking changes is part of “stuff”. I can make the argument both ways. 😄 |
have you tried just doing: const fetch = (...args) => import('node-fetch').then(module => module.default(...args)) ...to see if it works? |
I think it should work but if exported by
Bundling of dependencies is totally optionnal. I used it because,
There is no doubling of codebase, you keep the same source code for both exports. Unlike what many people thinks, bundling is a very good practice for performance (even without external dependencies). The public package is tinier and it loads faster (less disk operations). Having both CJS and ESM exports allows all existing consumers to keep their codebase without changing any line of code. I think that it's very important when you maintain a library which is very popular like I'm also a big fan of ESM but I think that we should be concerned of existing codebases before making such a breaking change.
We should think of when v3 will be stable. You will have a lot of issues of people upgrading their dependencies and having trouble with their existing codebases. Thanks for all you comments. |
You can still load headers, req, res etc but it has to be done async. Another way if you still have a large codebase is to bootstrap your application to load node-fetch first and then run your own application // main `./bootstrap.js` file in cjs format
// Load node-fetch first.
import('node-fetch').then(mod => {
// Assign fetch to global namespace (could also assign it to a
// cjs factory that can be require'd if you don't want to pollute global namespace)
const {default: fetch, ...rest} = mod.default
Object.assign(globalThis, rest, { fetch })
// Run your main legacy application that is built entirely in commonjs format.
import('./index.js')
}) (ofc i know this dose not work for everyones needs but cjs and esm can still go exist with each other - you do not have to update your entire codebase to esm in one go to be able to use packages that have been targeted for ESM only)
That won't solve the fact that |
Originally posted by @gbiryukov in #1226 (comment) |
That is a problem with webpack, not with node-fetch. Since every supported version of Node.js supports ESM modules, there is no point adding CommonJS bundles. |
Indeed. The question is how many users of webpack will face this issue and spend their time to find workaround (which is not extremely simple due to couple of reasons). Personally to me it seems like majority of the users of Cjs build or at least simple guide how to use |
If people are building with webpack, will it not simply inline this entire module, which will eliminate the problem altogether? Also, webpack has support for outputting a bundle that uses |
when app bundled for target
true but this feature only available in experimental mode and only for v5 users which have pretty small adoption. I understand intention to keep things simple and forget about cjs (and other not most up to date stuff) but just makes such a pain to use |
If you are using webpack to bundle stuff then it most likely mean frontend apps? in which case window.fetch should be used instead of node-fetch. (something like isomorphic-fetch)
That is what we are trying to achieve with #1236 |
I've added some context above
So the need for |
I suppose you are talking about native react (for phones) and not the web version of react? |
I am talking about web version of react. It uses own syntax (JSX) that is not part of ES6 so it needs to be transformed into ES6 before running in both browser and node environments |
During this transformation process, Webpack would transpile node-fetch to CommonJS so that it can run in older browsers. In fact, it should be noted that this is node-fetch. |
This is a bad decision. ES modules are not ready for prime-time. And its not trivial to shift large projects to ES modules. It just cuts off new bug fixes and features to non-ESM packages. Take the And if you use any kind of import/require hooks, well the API for this is not stable and will change soon. So if you rely on these features, not only is it a lot of dev work, you simply cannot migrate because ESM is not ready. Same discussion here regarding Sindre's packages which probably kicked off this chaos. Conditional exports was built into Node.js to avoid this pain...and every library just ignores it. |
FYI, AWS Lambda (and other platforms based on Lambda, e.g. Netlify functions) still doesn't have ESM support in its Node14 implementation. So anyone building Lambda functions will need to package their code as CJS. See https://stackoverflow.com/questions/66676555/nodejs-14-x-native-aws-lambda-import-export-support/66688901#66688901 for updates. When I have time, I'm going to investigate using esbuild (probably via serverless-esbuild because my Lambda functions are built with the serverless framework) to transform ESM code into CJS so that AWS Lambda will run it. I've never used esbuild before so I'm not sure this will work, but it seems easy enough to try it. Sadly, I suspect that the ESM-only approach that node-fetch and Sindre are doing is what will be needed to force the rest of the ecosystem (looking at you, AWS!) to get their own ESM support shipped. It's like a big game of tech chicken, and hopefully ESM will win the game before everyone crashes! :-) |
Another use case that brought me here is compiling to commonjs for unit tests only, to be able to spy on exported functions (see discussion in jasmine/jasmine#1414 (comment) and https://jasmine.github.io/pages/faq.html#module-spy) |
@justingrant I would recommend you to bundle your code before shipping to Lambda in any case, it makes a ton of difference (in my experience) with the size of the Lambda bundle, and the cold boot time. You can use e.g. For Lambda I also think it's very good that the ecosystem is aggressively moving to ESM, because without that I don't think that AWS will implement support for it anytime soon. Many packages not working without it will hopefully pressure them to fix ESM support (which by the way would be very easy for them, if you dump their startup code you can see that it basically is just to replace a |
Honestly this is the most short-sighted thing I think I've ever witnessed in the JavaScript space. All these people pushing ESM as if its "for the greater good" ... I can't think they have ever worked on an even slightly large Node.js project with diverse dependency trees. Or realize that Webpack itself is still broken when trying to make your project fully ESM as output... Thanks to you guys- I have spent the past week of wasted time trying to resolve these And its often not our choice at all- we might just update a single module for a security release- and that module has now made use of a new version of a module ( I can appreciate trying to move JavaScript forward- we all want to work with pure modules and modern JS too- but this whole thing is at least a year too early, modules has only just come out of experimental in Node.js (relatively speaking) and Webpack 5 doesn't even support modules (its under an experimental flag, but even then it fails to package your code as ESM in many cases). This is literally a dead end for users. |
we have...
That's webpacks problem. We considered packing node-fetch our self to have dual suport but the conns outweighed the pros
v3 have only been out for 2 days...
you often have a choice, you can stick with v2.x if you like, it's very stable, it has fixed many bugs and haven't been touched much |
This is the lack of empathy and understanding of the JavaScript ecosystem as a whole that I'm talking about. I don't think you realize just how many projects have Webpack as a core part of their packaging pipeline. This is why I'm saying its premature, there are options given to you to make this transition easier on everyone (such as pointed out by @vjpr, conditional exports)- but you have decided to take the "scorched-earth" policy instead.
While I appreciate that... as I said we often don't make that choice directly ... This is why I ask if you have ever really worked on large JS projects, possibly monorepos that link up and use 100s of diverse dependencies, some of which have decided to update a dependency to a version that uses ESM (unknowing to the library author how that might effect downstream users because there won't be any errors popping up when he bundles his library) which now causes all projects which update (sometimes even a minor version, because their API hasn't changed at all) to suddenly be faced with all kinds of errors. Now imagine that this kind of thing happens- but its 3 dependencies deep. This is a huge mess, and I don't think its fair on users who are simply trying to be productive to tell them to now chase down all these version numbers and fix everything, or make PRs to all his dependent projects to back-port needed functionality to the version that makes use of an older non-ESM dependency... especially when popular tooling, such as Webpack 5 (probably the most used JS bundling tool by far) still does not support outputting our projects to ESM to make this transition easier. |
Please. Please. Please.
I love and using I totally agree on your foresee of the future of ESM. But no one foresee 2020. |
Although I find a way out, but
@jimmywarting |
Actually there is one.
I love and fully using |
Hello everyone, Just some figures from
It's only So this decision is impacting millions of developers around the world who will encounter an unexpected issue when migrating to This decision will cost millions of hours (and dollars) in the javascript development ecosystem. You will directly impact the job of millions of developers and companies. Finally, all the time lost on This could become the worst decision in the node.js ecosystem in 2021! And this decision is taken only because you don't want to introduce a transpiler (here In this PR, I suggest to not-only transpile the code but also bundle it (for performance). If you don't like bundling, we could only transpile the code. Transpilation is a safe process, it can be tested during the CI and it will not take you too much time. Do you agree if I update this PR to make a new suggestion with only transpilation (no more bundling) and CI testing? I'm also a huge fan of But here we talk about maintaining a globally used library. You can't just impose such a breaking change to millions of people because it's your (own) conviction. Please be humble and reconsider your convictions. Edit: you should also think about TypeScript users who could not mix |
This comment has been minimized.
This comment has been minimized.
Should the title to If concern about release size, can also use |
the release size is not the only thing on my mind. it's about not having 2 instances of whatwg:streams or anything else for that mater MattiasBuelens/web-streams-polyfill#70 (comment) A ReadableStream created from version 2.1.1 can not pipe data to a WritableStream constructed by version 2.1.2 |
@SomaticIT is there anything preventing you from staying on 2.x as long as you need CommonJS, and then upgrading to 3.x when you are switched over to ESM? As was said before:
|
@jimmywarting Maybe just rename As it's is breaking update for current ecosystem. FYI, I'm using pnpm. I updated without see any warning weeks ago.. |
@jimmywarting, There is no problem to stay with unbundled dependencies. We just need to set them as external when running @LinusU, Yes for 3 reasons:
@LinusU, Moreover, I'm not the only one to considerate. As I said before, there are millions of projects which uses |
Could we fix this in the 2.x branch?
As have been stated in this very thread, we are open to fixing bugs and security issues.
As I'm sure you are aware, many other maintainers, including Sindre with his 1000+ packages, are switching to ESM only. I think that you will need to reconsider this approach if you want to stay with CommonJS...
I think that the same thing I said to you applies to other projects: is there anything preventing them from staying on 2.x as long as they need CommonJS, and then upgrading to 3.x when they are switched over to ESM? |
in case of universal apps (that runs both in browser and in node) v3 is very desirable simply due to better compliance to the the spec so all features can be safely used like |
Hello everyone, Just to let the community knows, after a lot of issues in my existing projects, I created a wrapper of node-fetch to use in The goal of this package is to help You can find it here: touchifyapp/node-fetch-cjs. If you are able to migrate to the official v3 release of node-fetch, I highly recommend to use the official node-fetch. This package is built to help users who could not migrate easily. This repository is automatically updated when a new version of node-fetch is released |
I'm wondering if anyone has any suggestions for if there is a way to use node-fetch v3 in my specific situation. I am currently on a project using webpack which outputs cjs because we are stuck using node 14 in an aws lambda. esm is only experimental in node14, so I can't switch the whole project over to esm. Just curious if there are any other options besides stay with v2 or switch to another library. |
@melissarh57 ESM should be supported all the way from v12.17 and cjs should be able to load esm only packages using async import. maybe this solution can work for you #1279 (comment) ? |
https://nodejs.org/en/blog/announcements/v18-release-announce/#fetch-experimental
$ node
Welcome to Node.js v18.0.0.
> fetch
[AsyncFunction: fetch] |
THIS IS A PROPOSAL
As explained in #1226, the fact that
node-fetch
v3.0.0-beta.10 is now pure ESM cause multiple problems with existing codebases.In JavaScript codebases, we need to use the async
import()
which can create a lot of changes.In TypeScript codebases (with cjs module mode), it is impossible to use
node-fetch
anymore.I think that ESM is the future but it is not as stable as we think and it is not ready to be used everywhere.
The best way to ensure that all existing codebases can migrate to v3 is to provide both
cjs
andesm
versions of this module.For this, I introduced
esbuild
which can transpile ESM to CJS and also can bundle the package.You can say: "node does not need packages to be bundled", which is true.
However, bundling is a good practice to reduce the module loading time, disk operations, memory footprint, etc.
It can also be used to bundle internal dependencies and so reduce the npm dependency tree (reduce install time, disk operations, memory footprint, npm hell, etc.).
What is the purpose of this pull request?
What changes did you make? (provide an overview)
Which issue (if any) does this pull request address?
#1226
#1217
Is there anything you'd like reviewers to know?
In this implementation, I bundle both
data-uri-to-buffer
andfetch-blob
into the final result allowing us to be dependency free.I think
data-uri-to-buffer
is safe to bundle because it's an internal dependency.However, I think
fetch-blob
should not be bundled because it could be used by the consumer.What do you think?
Note 1: If we do not want to bundle
fetch-blob
, we have to provide a CJS export for it.Note 2: If we do not bundle
fetch-blob
, the install size will be very large as mentioned by @akaupi in #1217. Maybe we could make fetch-blob dependency free by usingesbuild
to bundleweb-streams-polyfill
.