-
Notifications
You must be signed in to change notification settings - Fork 362
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
Should modern
option be orthogonal to module format?
#618
Comments
Hi Charles - just a foreword, this reply got a little long. My apologies in advance! In terms of a straightforward answer to your petition, you can actually already use a Now, on to the underlying question: how can we publish modern code to npm without breaking everything? FWIW, unless you're specifically targeting only Node.js, publishing a package as modern-only is going to create a lot of confusion. Create React App transpiles node_modules by default, but no other popular bundler configurations do - that means most projects simply aren't set up to handle modern code in npm packages. That's not to say I don't want to fix the whole "modern npm packages" issue - very much the opposite. I believe there is a way forward, but from both my own experience and in talking to the folks who maintain downstream tooling, publishing modern-only browser packages isn't going to get us there. Instead, I think we may be in a position to standardize on the existence of an Export Map as the indicator that a package provides modern code. Because Export Maps have a designated mechanism for falling back to the package.json However, there's a huge missing piece. In all of these approaches, from modern-only packages to backwards-compatible multi-mode packages, the syntax we refer to as "modern" is an arbitrary point-in-time decision that happens on a per-package basis. Scale that up to the whole ecosystem and we end up having no way to know if a package is ES3 or ES2020 - bad for performance, and potentially impossible to work with. The "modern" name used by Microbundle refers to "the JavaScript syntax supported in all browsers that implement <script type=module>". It's a mouthful, and my fingers are tired from having to explain that sentence to hundreds of people at this point, but it's the only syntax "cutoff" (or target) that is based on real-world browser support constraints. |
Thanks for the insight into where microbundle is coming from. Indeed, the only thing we can be sure about what "modern" JavaScript is that it will eventually be succeeded by "post-modern" JavaScript. Sometimes I feel what's really needed is a meta-language for packages to declare unambiguously which language features they use so that a decision on what transformations to make can be deferred all the way until very last moment where an application itself is assembled. Because really, only the app knows what its target is. As it stands these days, it feels analogous to shipping C++ code with partially evaluated preprocessor macros. Again, thanks! |
That's a great analogy. FWIW here's the current best we can do at providing that mega-language based on properties already supported in Node.js (and coming soon to webpack & rollup): // package.json
{
"main": "./es5.umd.js", // ← lowest common denominator (Node LTS, IE11, etc)
"module": "./es5.esm.js", // ← optional, enables tree shaking in legacy bundler configurations
"browser": "./es5.browser.umd.js", // ← for all browsers (legacy support)
"exports": {
"browser": "./es2017.browser.mjs", // ← for modern <script type="module">-supporting browsers
"import": "./es2017.mjs", // ← for Node 12+ and <script type=module>
"default": "./es5.umd.js" // ← for Node 12+ require()
}
} My plan for the next version of Microbundle (our 1.0) is to use Export Maps to completely control what gets bundled. A microbundle package would look like this: {
"name": "foo",
"main": "./dist/foo.es5.umd.js", // the fallback/web UMD bundle
"module": "./dist/foo.es5.js", // the fallback/root ESM+ES5 bundle
"type": "module",
"exports": {
".": { // main entry module
"browser": "./dist/foo.browser.js", // <script type=module> + MJS
"module": "./dist/foo.js", // modern + MJS
"require": "./dist/foo.cjs" // modern + CJS (optional)
"default": "./dist/foo.umd.js" // modern + UMD
},
"./hooks": { // additional entry modules (instead of passing files!)
"browser": "./hooks/index.browser.js",
"module": "./hooks/index.js",
"default": "./hooks/index.umd.js"
},
} Microbundle will validate the export map configuration, which means every Microbundle package will be correctly loadable in Node and bundlers going forward, with no manual process to follow. Building the above would produce:
|
Perhaps this ticket could be reopened? I'd be interested in a 'modern' CJS variant, as I'm still trying to support CJS for (modern) Node.js versions. My library targets multiple runtimes, so I don't think I can use I do eventually hope to go ESM-only, but it seems the timing is never right... |
Are you sure you need "browser CJS"[1] output? Pretty much every bundler for the past 5+ years will prefer ESM if your library provides it, often leaving CJS as being exclusively consumed in Node. We don't clear your output directory or anything before a build, so you could just run Microbundle twice, the second time with [1]: By "browser CJS", I mean CJS code that will be fed into a bundler for use in browser envs -- obviously CJS itself won't ever be executed in a browser context. |
Maybe I don't! I'll ask around. Would my "type": "module",
"sideEffects": false,
"exports": {
"types": "./dist/pkg.d.ts",
"node": {
"types": "./dist/pkg.d.ts",
"require": "./dist/pkg.cjs" // built with --target node
"default": "./dist/pkg.modern.js"
},
"default": "./dist/pkg.modern.js"
},
"types": "./dist/pkg.d.ts",
"module": "./dist/pkg.esm.js", I'm supporting Node.js v18+, modern browsers, and Deno if that makes a difference. |
That should work fine, but is there even a distinction between your browser and Node builds if you're supplying the same If not, you could go even simpler: {
"type": "module",
"sideEffects": false,
"exports": {
"types": "./dist/pkg.d.ts",
"require": "./dist/pkg.cjs" // built with --target node
"default": "./dist/pkg.modern.js"
},
"types": "./dist/pkg.d.ts",
"module": "./dist/pkg.esm.js",
"main": "./dist/pkg.cjs" // though this has a tiny bit of danger in old bundlers & Node versions that don't recognize `.cjs` -- could flip the module type if necessary
} The question is, do you need to ship a CJS bundle meant for the browser (say, using |
We recently decided that we wanted to drop support for older browsers and move to not transpiling generators and async/await. Our problem is that (using the recently released 0.12) we can't seem to generate a modern JavaScript that is transpiled to both commonjs and esm, and it's unclear whether it is possible to do this with
microbundle
today.Given the churn around #518 #582 #570 and what is hopefully the final fix with #605 it has me wondering if things would be simpler both internally and externally if "modernity" were treated as type of content rather than a module format.
In other words, the module format could vary independently of the transpilation target. That way you could run something like:
And microbundle would emit a matrix of modules crossed by content type and module format such as:
This would give package maintainers the ability to target modernity (or lack thereof) and be able to deliver easily regardless of the consumer's preferred module format.
The text was updated successfully, but these errors were encountered: