17 ms·
ES modules are terrible
- forty 5y agoI have been doing nodejs backend development for the past 10 years and I have no idea why ES module are needed and who is using them. I assume this is a front end thing. We are using typescript which is using "import" syntax, but as far as I know, it's still transpiling to good old "require".
- throw_m239339 5y ago> I have been doing nodejs backend development for the past 10 years and I have no idea why ES module are needed and who is using them ES modules are part of the Ecmascript spec. DENO uses them. Node.js modules aren't part of the ES spec.
- forty 5y agoI see, so a front end thing indeed :) (and yes deno, which I feel the main purpose is to bring the problems and debates of the front end devs, in an otherwise much saner js backend end world ^^ )
- deleted 5y ago[deleted]
- andrew_ 5y agoYou can tell TS to output ESM. Try using top-level await with TS and you'll run into that. The configuration options are vast.
- arh68 5y agoAgreed. So much breakage for so little. If I were teaching JS today I don't know if ESM is worth covering while CJS works at least as well. Maybe next year.
- tolmasky 5y agoThe “it works without a tool chain” is in fact a ridiculous impractical hypothetical that no one should actually attempt, and yet it continues to make this spec more complicated and unwieldy. For example, to address the obvious performance problem of dealing with loading dependencies, the “<link del=modulepreload>” tag was added, which you’re supposed to include for each individual dependency in your html to let it know to start fetching it ahead of time. So we’ve literally gone full circle and arrived right back to where we started with a script tag for every JS file being replaced with a link tag for every JS file. “But you don’t have to manually do that! Your build tools can just insert the 100 link tags in your HTML file!” I thought all this was to avoid a JS tool chain! If I’m running a build tool I’ll just have it generate one concatenated and minified artifact that performs way better, not this mess! Here’s the documentation if you’re interested in this hilarious feature: https://developers.google.com/web/updates/2017/12/modulepreload#but_what_about_modules_dependencies https://developers.google.com/web/updates/2017/12/moduleprel... Not to mention the security aspects: there is no subresource integrity for imports, so it’s less secure than bundling or using a script tag with CDNs. The point about it being a new syntax is also very valid. Everything import patterns do is almost identical to destructuring, so we should have just extended that feature instead, especially because I do wish destructuring could do those things. For example, if destructuring had an “everything” pattern to complement the “rest” pattern: const { x, …rest, *original } = something(); Where “original” now just contains a reference to the actual returned object, instead of having to break that pattern up into two declarations since the moment destructuring takes place the original object becomes inaccessible. This would have of course given us the “import * as” ability, but is again a feature I regularly find myself wanting everywhere. Not to mention this makes writing code transformations even harder as JavaScript's huge syntax keeps growing and requiring tons of special cases for almost identical statements. The semantics of imports are also very confusing to beginners, as they implement yet another unique form of hoisting. It is so weird that despite being allowed anywhere in your code, they run first. Notice I didn’t say they fetch first, they run first. So for example, the following code is broken: process.env.S3_LOCATION = “https://…”; // The below library expects this as an environment variable. import download from “s3-download”; Oops! Your env variable gets set after every line of code in the import chain of s3-download runs! So bizarrely, the solution is to put the first line in its own import, and now it will run first import unused from “./set-env-variable.js” import download from “s3-download” If the rule is that imports must run before any code in the file, then why not restrict the statement to only being at the top of the file? What is the purpose of allowing you to put all your imports at the bottom? Just to make JavaScript even more confusing to people? Imagine if “use strict” could appear anywhere in the file, even 1000 lines in, but then still affected the whole file. It was already the case that people found function hoisting, "var undefined" hoisting, and the temporal dead zone of let/const (3 different kinds of subtly different hoists) to be confusing in a language that prides itself for being able to be read "top to bottom", why add a fourth form of hoisting? Anyways, the list of problems actually continues, but there is widespread acceptance that this feature would not have been accepted in its current form if introduced today. But for some reason everyone just takes a “but it’s what we got” position and then continues piling more junk on top of it making it even worse.
- throwaway2077 5y agoquestion: // ... if (condition) { const x = require('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart') // do something with x } // ... assume I don't give a fuck about nerd bullshit and I just want the code to be simple and the program to run fast (which it does when !condition because it doesn't need to load hugeFuckingLibrary), can I replicate this behavior with ESM?
- jitl 5y agoYou can replace that require with `await import(‘giantLibrary’)` but now your function needs to be async, and so do all of its callers. This is needed because it’s unacceptable to block the UI thread synchronously importing code in the browser, but in CLI programs not being able to synchronously require is a bit annoying.
- throwaway2077 5y agothat's a shame, I hoped there was going to be a way to do that and it was simply not implemented in node at the time I was looking into it
- tehbeard 5y agoWell since you asked so "fuckin" nicely /s if( condition ){ import('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart').then( x => { //do something with x }) } > ...nerd bullshit... I hate to break it to you darling, but programming is nerd bullshit. edit: alternate that might too much "nerd bullshit", but uses async/await if the surrounding code is an async function: async function doSomeStuff() { if( condition ){ const x = await import('../../../hugeFuckingLibraryThatTakesSeveralSecondsToLoadUponColdStart'); //do something with x } }
- throwaway2077 5y agono bro, programming is programming and nerd bullshit is nerd bullshit. the arguments I see in favor of ESM over CJS fall into the latter category, at least on the node side of things.
- junon 5y agoThis is a criticism of the tooling, not of the language feature. This is like saying "binding two pieces of wood together is terrible" and using the fact that screwdrivers are poorly designed as your main argument.
- api 5y agoIt’s puzzling to me why there isn’t more effort on developing front end Go or Rust frameworks that compile to JS or WASM. It would be a chance to work in a real language instead of this trash.
- aliswe 5y agoi have sympathy for your projects that will need maintenance. but i am making the observation that you're starting to get old - you're being quite bitter over something which is clearly bigger than all of us. on a personal note, im making a cms with es modules and couldnt be happier.
- jokethrowaway 5y agoES Modules was what turned node.js into legacy for me, same as python 2/3. Plus transpilers are so slow, it's embarrassing (albeit things are improving with tools written in rust). As someone who's been doing frontend for 20 years and node.js for 10 years, JS development has never been so crap like now. After attending a conference talk about how the TC39 works, I understand why that's the case. TC39 is basically a bunch of engineers from big tech companies who can afford to waste productivity to follow the whims of whatever the group decide. It's completely detached from reality. They operate on a full consensus basis, which means everyone needs to be onboard with the decisions - and if you want your changes to be approved in the future, you'd better play nice with the current change as well. To be honest, I can't wait until browsers get replaced with some native crossplatform toolkit or frameworks in other languages become popular so that we can finally leave JS alone.
- paufernandez 5y ago> To be honest, I can't wait until browsers get replaced with some native crossplatform toolkit That sounds like Flutter to me.
- deleted 5y ago[deleted]
- axismundi 5y agoDon't bundle. The only reason for bundling is too many requests to the server. Use HTTP/2 instead.
- crooked-v 5y agoIf you do that you're stuck with round trip times all the way down the dependency tree. HTTP/2 reduces the overhead of that, but doesn't eliminate it, so now you've added a bunch of loading time to your site.
- axismundi 5y agoI thought that as well, until I read this: https://www.sitepoint.com/file-bundling-and-http2/ https://www.sitepoint.com/file-bundling-and-http2/ https://medium.com/@asyncmax/the-right-way-to-bundle-your-assets-for-faster-sites-over-http-2-437c37efe3ff https://medium.com/@asyncmax/the-right-way-to-bundle-your-as...
- crooked-v 5y agoI don't see a counterargument here. "It's fast" doesn't change the fact that using 'native' imports instead of bundles means you're still adding the round trip time for the browser to request each set of dependencies all the way down the dependency tree.
- draw_down 5y agoMadness.
- RedShift1 5y agoWorks... as long as you are next to the server...
- somehnacct3757 5y agoI keep thinking how QUIC / HTTP/3 would go nicely with ESM in the browser (via script tags with type=module) for simple sites. A webmaster could totally avoid the complexity of learning a JS tool chain. Right now even 1000 lines of JS has you reaching for a bundler. It would make shipping a small html+css+js site again as simple as dragging files to your webserver.
- e1g 5y agoBundling will not go away as it solves a different problem of how to best distribute the app to final users. When authoring code, you want to have many small files so you can keep related logic blocks isolated from the rest. When distributing the code, you want to ship a few larger files to reduce network overheads. Any non-trivial frontend app will call code from 100+ files, and your browser is tuned to request these files ~serially (or in serial batches of 8-10) which becomes frustrating very quickly even on localhost.
- joepie91_ 5y agoWorse, even if they were all fetched in parallel, you would still see terrible loading times simply because a dependency graph can only be traversed depth-wise serially. Doesn't matter what network protocol you use or how parallel it is.
- spankalee 5y agoThis is not true at all. For every module you can parse out and load the module's imports in parallel. As you traverse the graph the known and loadable module frontier can grow much wider. The only way it would be serial is if every module only imported one other module.
- joepie91_ 5y agoNote how I was specifically talking about depth-wise.
- TekMol 5y agoES Modules are great. Building JS applications is so much speedier, leaner and more fun now that they are supported widely. One fallacy the author falls for is that they think one needs a build step "anyway" because otherwise there would be too many requests to the backend. Loading an AirBnB listing causes 250 requests and loads 10MB of data. With a leaner approach, using ES Modules, the same functionality can be done with a fraction of those requests. And then - because not bundled - all the modules that will be used on another page will be cached already. I use ES Modules for all my front end development and I get nothing but praise for how snappy my web applications are compared to the competition.
- forty 5y agoI think the trick is mostly not to have a shitload of dependencies. If you have to load a bunch of huge frameworks, whether it's bundled or you have to download thousands of files one by one, it's going to be slower than not doing it at all :)
- eyelidlessness 5y agoIt depends on your bundler and config. You might have “zero” dependencies, but depending on how the code is split you might end up with thousands of small imports nested quite deeply.
- deleted 5y ago[deleted]
- catern 5y ago>And then - because not bundled - all the modules that will be used on another page will be cached already. Why isn't anyone else mentioning this feature? I'm not a browser developer but this seems like a clear win, and indeed makes bundling unnecessary. I'm assuming that it's shared between domains, too - or are people's dependencies so fragmented that there's basically no sharing between domains?
- qudat 5y ago
- rado 5y agoHTTP/2 uses a single connection for all modules, no?
- crooked-v 5y agoIf you use 'native' imports in that way instead of bundles, you're still stuck with artificially delayed loading times because the browser has to parse your code, request the first layer of dependencies, parse that code, request the second layer of dependencies, etc.
- cryptica 5y agoIt just sucks that development community decided to double down on bulky build tools instead of trying to optimize server environments to leverage advances like HTTP2 server push to optimistically serve dependencies without latency. It's particularly strange when you consider how popular Node.js is as a server-side environment and how easy it would be to accomplish this since the server is able to interpret JavaScript natively to quickly figure out the client-side dependency tree. My inner conspiracy theorist suspects that maybe the powers that be don't want to allow plain JavaScript to extend its primacy over the web. The way things went makes no sense. Computing dependency trees on the server-side and using it to optimistically push scripts to the browser would have been be far simpler and less hacky than computing source maps, for example. Optimistically pushing server-side resources was supposed to be the whole point of HTTP2...
- austincheney 5y agoThe reasoning presented is only valid if you are stuck holding a bunch of dependencies making use of old conventions. At that moment the complaints about the module approach become a very real concern. That said the problem isn’t modules are all. It’s reliance on a forest of legacy nonsense. If you need a million NPM modules to write 9 lines of left pad these concerns are extremely important. If, on the other hand, your dependencies comprise a few TypeScript types there is nothing to worry about. So it’s a legacy death spiral in the browser. Many developers need a bunch of legacy tools to compile things and bundles and build tools and all kinds of other extraneous bullshit. Part of that need is to compensate for tooling to handle modules that predates and is not compatible with the standard, which then reenforces not using the module standard. When you get rid of that garbage it’s great. ES modules are fully supported in Node and the browser.
- fullstackchris 5y agoAnd yet, the reality IS that 90% of the web is using legacy stuff - heck, even something like 50% of the web still has jQuery on it. (haven't checked the figure in a while, but I guess it is still close to that figure). I think the true anger is that something so essential and basic to JS development has this giant breaking change if you want to switch over to ESM - there's no reverse compatibility or fallback - it just breaks.
- austincheney 5y agoThe solution is some soul searching. Do you really need Babel and Webpack to build a web app? The answer is of course an astounding YES! Most developers cannot add text to a page without JSX, which therefore means React and everything it requires. So when you dig even deeper this is really a people and training problem.
- lampe3 5y agoNot sure it the author tried a new build tool like vite, esbuild and so on. Working on large projects and having everything first loaded and then you can load it in the browser is a waste of time that every web developer has every day. Some real world times FOR DEVELOPMENT: Storybook first load: 90 sec, Storybook after first load changes: 3 sec, Vue App first load: 63 sec, Vue app change after that: 5 sec, Vue App with Vite first load: 1sec, Vue App with Vite after that: the time it takes me to press command+tab to switch to the browser Do we really have people that use unminfied unbundled esm in production? If Yes, please comment why? I would also ask the author what about cyclic dependencies? ES Modules resolve them automatically. Something which in large code bases can happen. Why do we still put it through babel? Because most of us don't have the luxury of not supporting old browser... https://caniuse.com/?search=modules https://caniuse.com/?search=modules Even if the not supported browsers for our company is 1% it is still a big chunk of money in the end. and this example: ``` app.use("/users", require("./routers/users")); ``` Really? this is "good code" having a require in the middle of a file? Also funny: The author is annoyed that rollup did not support tree shaking in commonjs and then complains that people are wasting time on esm. Maybe the rollup team does not want to waste time on commonjs? Also then he points to a package which did not got any update in 3 years and would make the hole process he complains is to complex even more complex by introducing a new dependencies. Sorry but the more I read that thing the more it sounds to me like a Junior Dev that does not want to learn new things and just likes to rant about things.
- joepie91_ 5y agoHi, author here. I'm going to ignore the personal attacks and simply point out that my dev build processes typically have a startup time of under 5 seconds even for large projects, and a rebuild time of under 500ms. This is with Browserify. If you are having very slow build times with your existing toolchain, the problem isn't the bundling, which is an extremely fast operation. It's almost certainly going to be one specific computationally-intensive plugin that you either don't need, or would also need if using ESM.
- lampe3 5y ago"Wie man in den Wald hinein ruft, so schallt es heraus" since your from NL it should be easy to translate. These heavy plugins are usually for old browsers to also run in them. That is the only job ob babel. CJS has some deeper problems. - Check if your fav CJS lib freezes the objects? - ESM is more http friendly (mime type)
- incrudible 5y ago"And then people go "but you can use ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature because loading a full dependency tree over the network would be unreasonably and unavoidably slow - you'd need as many roundtrips as there are levels of depth in your dependency tree - and so you need some kind of build step anyway, eliminating this entire supposed benefit. That build step is ideally performed by something like rollup or esbuild, which means I use import/export anyway. If you still use Babel, I feel bad for you son, I've got 99 problems but Babel ain't one. I don't care if the old stuff is not supported, simply deleting 98% of the code in the JS ecosystem would be a step forward. Perhaps that's a minority view, but none of these arguments fly with me.
- emersion 5y agoI'm using ES modules for a webapp I maintain, and it's just nice to be able to run it without any build step. Just fire off a local static HTTP server and you're good to go. There's an optional production build step which can be used if desirable.
- incrudible 5y agoI would admit that this gets pretty slow even with a modest amount of files. If you use rollup/esbuild, you can have a very fast build step that may amortize over the increased page load times.
- deleted 5y ago[deleted]
- dgb23 5y agoIn my opinion as a working web developer, ES modules are half-backed, deceptively simple, do not solve problems consistently and are not built on hard acquired wisdom from other languages. 1) JavaScript could have simply stolen an already good solution. For example namespaces (ex: Clojure/Script, Typescript, even PHP to some degree) provide a powerful mechanism to modularize names - by disentangling them from loading code. They make it straight forward to avoid collisions and verbose (noisy) names. In Clojure namespaces are first class and meant to be globally unique. This implies long-term robustness. 2) Loading modules dynamically should be the _default_. The whole point of JavaScript is that it is a dynamic language. The caveats, hoops and traps that we have to sidestep to for example run a working, real REPL during development is astounding. If you want to be dynamic, go _all_ the way and understand what that means. Yes, it's a tradeoff to be a dynamic language, but why take the worst of both worlds? 3) Like 'async/await', 'class' and many browser features such as IndexedDB it is neither designed from first principles nor fully based on past wisdom. Many things in the JS world smell of "lowest common denominator". Way too much effort is focused on the convenience side of things and way too little on the leverage side.
- cookiengineer 5y agoThis thread and summary are written by someone who has no clue what they're doing in ECMAScript; and who's probably enjoying the fucked up mess that the babel ecosystem created. I'm not gonna dig into that, because reading any polyfill in babel's ecosystem speaks for themselves on how messy, hacky, and actually not working-as-specified most parts are. Instead I'm gonna try to go back to the topic. I think that in practice these are my pain points in using ESM regularly without any build tool. I'm using ESM modules both in node.js and in the Web Browser via <script type module>: - package.json/exports "hack" works only in node.js and not in the Browser as there's also no equivalent API available. This hack allows to namespace entry points for your library, so that you can use "import foo from 'bar/qux';" without having to use "../../../" fatigued paths everywhere (that also might be different in the Browser compared to the nodejs entry points). - "export * from '...';" is kind of necessary all the time in "index" files, but has a different behaviour than expected because it will import variable names. So export * from something won't work if the same variable name was exported by different files; and the last file usually wins (or it throws a SyntaxError, depending on the runtime). - Something like "import { * as something_else, named as foobar } from 'foo/bar';" would be the killer feature, as it would solve so many quirks of having to rename variables all the time. Default exports and named exports behave very differently in what they assign/"destruct", and this syntax would help fix those redundant imports everywhere. - "export already_imported_variable;" - why the HECK is this not in the specification? Having to declare new variable names for exports makes the creation of "index" files so damn painful. This syntax could fix this.
- Ginden 5y ago> - "export already_imported_variable;" - why the HECK is this not in the specification? Having to declare new variable names for exports makes the creation of "index" files so damn painful. This syntax could fix this. You can do: export {already_imported_variable}
- cookiengineer 5y ago...which is a default export, not a named export, as I already explained. My point was about the lack of exporting named exports without the need to declare variable names. Your solution will work only once in a file, therefore it is useless to batch-export lots of imports for the mentioned use case of an "index" file that exports all your classes and definitions.
- jitl 5y agoThe things I dislike the most in software development is dogma, holy wars, and religious crusades about technology practices. I’m not sure to the extent that this happens in other ecosystems, but it seems to happen quite a bit in JavaScript circles. You can ignore these for the most part if you use boring tools and don’t chase the new frameworks-du-jour, but in the case of ESM versus CommonJS I am starting to feel the fire of this war in my dependency graph. My solution in NodeJS programs for now is to use an `esbuild` -based require hook to transpile all the files we import or require into CommonJS on the fly. We need esbuild anyways to run Typescript code without a build step, and combined with basic mtime based caching, it’s fast enough that you really don’t notice extra build latency especially on a second run — much MUCH faster than a Babel require hook. I plan to tune back into this issue once the average comment is more measured and thoughtful, and the ecosystem tooling for dealing with the migration has evolved more.
- andrew_ 5y agoI really, really loathe how major packages in the ecosystem are "We're ESM now, deal with it, sorry about your luck," and forcing the issue. It's arrogant as hell. A hard fork of Node for ESM would have been a much better path (e.g. Deno) That said, the OP's rant is more emotion than fact. > And then there's Rollup, which apparently requires ESM to be used, at least to get things like treeshaking. Which then makes people believe that treeshaking is not possible with CommonJS modules. Well, it is - Rollup just chose not to support it. Rollup was created specifically for ESM. It's not been thrust onto the ecosystem or into anyone's tool chain. One uses it specifically for ESM, and plugins that bolt on for added functionality if they apply. Trying to hammer a nail with a paintbrush doesn't make the paintbrush a bad thing - you just chose the wrong tool.
- devmunchies 5y ago> loathe how major packages in the ecosystem are "We're ESM now, deal with it, sorry about your luck," and forcing the issue I wasn’t able to use the latest version of node-fetch in a node.js script since it doesn’t support commonjs. The project literally has “node” in the name and doesn’t support default node.js.
- theprotocol 5y agoI just encountered this. FYI You can use v2 which still retains commonjs support.
- andrew_ 5y agoI've ended up using last major versions as well. I plan to move to Deno anyhow, and authors like sindresorhus are at least applying security updates to the major version before the switch.
- spankalee 5y agoWere you not able to convert the script to a module, or dynamically import() node-fetch? Since you're using it for an async operation anyway, dynamic import should have worked quite well.
- aurelianito 5y agoBoth CommonJS and ES6 modules suck. The way things should have been is requirejs. Modules are defined and loading using an API instead of having reserved words. It's really sad what happened to modules in JavaScript.
- spankalee 5y agoThis post is terrible, actually. CommonJS was never going to be natively supported in browsers. The synchronous require semantics are simply incompatible with loading over a network, and the Node team should have known this and apparently (according to members of TC39 at the time) were told their design would not be compatible with future a JS module standard. So the primary thing that JS modules fix is native support, and for that you need either dedicated syntax or an AMD-style dependencies / module body separation. AMD is far too loose (you could run code outside the module body), so dedicated syntax it is. Everything else flows from there. I really hate how people blame the standards instead of the root cause which is Node not having taken the browser's requirements into consideration. Culturally, I think that's mostly fixed now, but it was a big problem early on in Node's evolution.
- alerighi 5y agoYes but who cares of native support in the browser? I mean, most JS stuff nowadays is transpiled, written in TypeScript, or if written in plain JS still transpiled anyway to support older browsers, and bundled in a single optimized file. Loading all the dependencies over the network to me is just inefficient, you will have hundreds of requests instead of a single one, you will load the full source not a minified and optimized one, I just don't see the point.
- spankalee 5y agoNative support matters so that we're not eternally required to use tools for even the simplest of cases. Being able to write two files with one importing the other with no npm or bundler in sight should absolutely be a feature of the native platform. And yes, in production you probably will want to bundle, but you probably also want to minify. Does that imply that we should require a minifier to even run any code at all, even in dev? No, of course not. By adding a standard and native support we allow for sites that work without bundling and bundling that can adhere to the standard and not have to even be configured because the input is standard and the output must preserve those standard semantics. That gives tool independence and simplifies usage of the toolchains, and that's a great goal to shoot for.
- aravindet 5y agoThere is a valid discussion to be had about whether the Node.js ecosystem disruption of moving from CJS to ESM is worth the benefits, but the assertion that it's technically worse isn't accurate. A few things ESM does better in Node.js: 1. Asynchronous dynamic import() vs. blocking require(): allows the program to continue while a module is being dynamically loaded. 2. Circular dependencies: ESM correctly resolves most of them, while CJS does not. [example below] I believe this is possible because ESM top-level imports and exports are resolved before JS execution begins, while require() is resolved when called (while JS is already executing.) 3. Reserved keywords `import` and `export` vs. ordinary identifiers require, exports and module: Allows tooling to be simpler and not have to analyze variable scope and shadowing to identify dependencies. I haven't really encountered #3, but I can say I've benefited from #1 and #2 in real-world Node.js projects using ESM. ---- Circular dependencies example: // a.js const b = require('./b.js'); module.exports = () => b(); // b.js const a = require('./a.js'); module.exports = () => console.log('Works!'); a(); Running this with "node b.js" gives "TypeError: b is not a function" inside a.js, while the equivalent ESM code correctly prints 'Works!'. To solve this in CJS, we have to always use "named exports" (exports.a = ... rather than module.exports = ...) and avoid destructuring in the top-level require (i.e. always do const a = require(...) and call it as a.a() elsewhere)
- eyelidlessness 5y agoHere is why ESM is better for static analysis than CJS: module.exports = { get foo() { const otherModule = require('equally-dynamic-cjs') if (otherModule.enabled) { return any.dynamic.thing.at.all } }, get bar() { this.quux = 'welp new export!' return 666 }, now: 'you see it', } setTimeout(() => { console.log(`now you don’t!`) delete module.exports.now }, Math.random() * 10000) if (Date.now() % 2 === 0) { module.exports = something.else.entirely } You can, of course, achieve this sort of dynamism with default exports. But default exports are only as tree-shakeable as CJS. Named exports are fully static and cannot be added or removed at runtime. Edit: typed on my phone, apologies for any typos or formatting mistakes.
- jeabays 5y agoEverything JS is terrible.
- Lhiw 5y agoWelcome to JavaScript, where everything's made up and the points don't matter.
- eyelidlessness 5y agoThe reason ESM is better than CJS or any other JS module system is because of the export keyword. Any discussion focused on imports is relevant but missing the significance of ESM.
- terracottage 5y agoThe worst part is default imports and the linting nazis who want you to use them. 1 file per thing is midwit code organization strategy for people with no actual sense for it.
- Aeolun 5y agoI kind of have to agree with the point about loading ESM in the browser. I tried doing this with one of the new fangled frameworks and seeing my browser work through like 5000ish required files was quite comical.
- emersion 5y agoIf you just have a handful of dependencies (which themselves have few to no transitive deps) then it works just fine.
- wereHamster 5y agoUnprocessed ESM in the browser makes sense for local development. We are collectively wasting millions of CPU hours (and developer time) waiting for our mostly unchanging dependencies to be processed. For production deployment though, I'd still prefer ESM in the browser, but not verbatim as they are coming from npm, but compiled, minified, and bundled in a way that strikes a balance between total number of modules, code duplication inside the modules, long-term cacheability etc.
- javajosh 5y agoThe more people start to internalize the truth that "if you ship it you own it" and stop adding dependencies and start removing them, especially if they come with their own wasteful dependencies, then ESM will make sense for everyone. Until then, you're right, devs have to go through unctuous mitigations.
- crooked-v 5y agoIt would be easier to do that if proposals for things like standard library functionality (not the contents of the standard library itself, just the syntax and technicalities of using it) were to go anywhere in, say, under five years.
- noduerme 5y agoJust as an aside... every web app I write these days starts with an index.html page that has a window["deploy"] bool at the top. If that's false, the first script just requires the unbuilt files. If true, it requires the compiled and minified version. I only rebuild when I'm ready to upload.
- lucideer 5y agoTL;DR: Breaking backwards compatibility is always painful but there's not one actual criticism of ES Modules as a spec here other than its incompatibility with CommonJS
- crooked-v 5y agoThe total inability to properly mock ES modules without experimental Node flags is a big one. It can turn unit testing into a nightmare if even one ESM dependency creeps in.
- djrockstar1 5y agoThat might sound irrelevant on the face of it, but it has very real consequences. For example, the following pattern is simply not possible with ESM: const someInitializedModule = require("module-name") (someOptions); Or how about this one? Also no longer possible: const app = express(); // ... app.use("/users", require("./routers/users")); Configurable modules and lazily loaded imports are both missing from the ES Modules spec.
- Ginden 5y ago> lazily loaded imports are both missing from the ES Modules spec. What do you mean?
- wereHamster 5y agoimport someModule from "module-name" const someInitializedModule = someModule(someOptions) A bit longer, but meh… const app = express(); app.use("/users", (await import("./routers/users")).default); top-level await is a thing now Actually, the first example could be rewritten as const someInitializedModule = (await import("module-name")).default(someOptions); That «simply not possible» statement is simply not true
- presentation 5y agoOr don’t even bother with the awaited import and instead import it at the top of the file, I fail to see why this is even an issue lol
- amadeuspagel 5y ago> And then people go "but you can use ESM in browsers without a build step!", apparently not realizing that that is an utterly useless feature because loading a full dependency tree over the network would be unreasonably and unavoidably slow - you'd need as many roundtrips as there are levels of depth in your dependency tree - and so you need some kind of build step anyway, eliminating this entire supposed benefit. That's not true with skypack, right?
- joepie91_ 5y agoIt's a fundamental technical constraint of any tool-less setup. At some point you need to traverse the dependency tree by parsing modules and following imports, and your choice is to do that either: 1) on the client, across the network, one roundtrip for every level of depth, or 2) in a build environment, directly on the filesystem Option 2 means you need some kind of build tool to make it work, and by that point it doesn't really matter anymore whether the tool just traverses the dependencies and makes a list of filenames, or also concatenates their contents into a bundle. And that is why the fundamental premise of ESM cannot work; there are no technical options besides those two. If you want to avoid network roundtrips, you must have build tooling. No way around it.
- javajosh 5y agoIt's important not to ignore the possibility that perhaps front-end dependencies are out-of-hand, and need to be reduced. ESM cannot fix a decade of bad practices enabled by front-end build bundlers. ESM isn't there to be a viable alternative to webpack. It's there to enable a different vision of application deployment where apps are smaller, and javascript gets css's transitive import() sub-resource distribution, avoiding the headache of a linear list of global scripts. I really like ESM because I like where it's trying to steer the community of browser application builders. I think front-end builds are terrible on many levels, not the least of which is the obfuscation of code that undermines one of the best features of the web's software distribution, which is its openness. And another major benefit of webapps is that none of the front-end languages require a build step! This makes iteration very fast; if you can make do without the the safety net of a compiler, you can enjoy the speed of not using the bundler.
- bricss 5y agoFirst you create problem with an article, then you fix it with your own magic tool, bravo! https://www.npmjs.com/package/fix-esm https://www.npmjs.com/package/fix-esm
- joepie91_ 5y agoBelieve me, I would much rather not have had to build that hack. But considering that I want my development tools to actually, y'know, work, what would you expect me to do?
- deleted 5y ago[deleted]
- NicoJuicy 5y agoJoepie91 did some work for me long ago If he says there is a problem, he didn't invent it. He knows his stuff
- Chyzwar 5y agoProblem is bothed node.js implementation that leaves most of existing applications without migration path. Even today it is not possible to create full ESM application front or backend. It is worse than python 2 to 3.
- throw_m239339 5y agoI would argue that Node way of doing things isn't in the ecmascript spec. The problem isn't ES modules, it's node.js. One could answer "well node.js existed prior es module specification". Irrelevant. DENO doesn't have this problem.
- eyelidlessness 5y agoNode ESM support has gotten a lot better through versions 12-17. The biggest problems for workflows that currently work “well” for CJS are: 1. --experimental-loader is more complex and less stable than --require. But it’s also a lot more robust. 2. There’s no equivalent to the require cache, which makes mocking and long running processes like watch mode challenging. This is partly a benefit, as it discourages cache busting patterns like those used in eg Jest which create awful memory leaks.
- theprotocol 5y agoAbsolutely this. It's not necessarily communicated that well in the article, but this is the main reason people are frustrated.
- xg15 5y ago> And then people go "well you can statically analyze it better!", apparently not realizing that ESM doesn't actually change any of the JS semantics other than the import/export syntax, and that the import/export statements are equally analyzable as top-level require/module.exports. ... "But in CommonJS you can use those elsewhere too, and that breaks static analyzers!", I hear you say. Well, yes, absolutely. But that is inherent in dynamic imports, which by the way, ESM also supports with its dynamic import() syntax. So it doesn't solve that either! Any static analyzer still needs to deal with the case of dynamic imports somehow - it's just rearranging deck chairs on the Titanic. I think while OP's right in theory, there is still a lot of difference between the two: ESM has dedicated syntax for static loading of modules and that syntax is strongly communicated to be the standard solution to use if you want to load a module. Yes, dynamic imports exist but they are sort of an exotic feature that you would only use in special situations. In contrast, CommonJS imports are dynamic by default and only happen to be statically analysable if you remember to write all your imports at the beginning of the module. That's a convention that's enforced through nothing and is not part of the language or even of CommonJS. As an exercise, try to write a static analyser that simply ignores dynamic imports and just outputs a dependency graph of your static imports - and compare how well this works with CommonJS vs ESM.
- wruza 5y agoHow browserify, webpack transformers and others are able to parse require()-s in the middle of a source file, but static analysers are not? These subtly erroneous arguments are the essence of this push. Look, we are maintaining X, Y and Z, and they’re unable to do that R, so it’s bad. No, it’s you making them unable to do that consciously.
- nosianu 5y ago1) importing module "require()" can take a non-static string. A variable or a string calculated at runtime. You can only statically check if that particular feature is not used, but there is no checking the entirety of what require() can be used for/with. require() is more like dynamic imports in Es modules that are awaited and not like the static ES modules. 2) exporting module The other issue is on the exporting module's side: You can do strange things with the "exports" object. ES module exporting is more strict to make it guaranteed statically analyzable.
- tbrock 5y agoWe recently went through this hell converting a NodeJS codebase to TypeScript. One reason many people willingly enter this hellscape is because we need ES modules for typescript. I say “need” because Typescript wont ingest types from “required” files, you have to import them as modules. So before we converted a single file to TS we has to audit all commonjs imports and exports to convert them to ES modules. I agree wholeheartedly that the end result was a fools errand. I would have rather spent the time adding support for importing types via a require which for some reason returns any “any” today.
- crooked-v 5y agoI think you've confused Typescript's own import/export system with ESM. It uses the `import` syntax, but it's not ESM internally, it's its own thing designed to ingest and export to multiple module types.
- draw_down 5y agoReally, you think GP just made this problem up?
- theprotocol 5y agoThe incompatibility is indeed exponentiated with TypeScript. There is currently no non-hacky way for using both legacy modules and ES Modules in the same project, and many libraries on NPM have moved to ESM-only. TypeScript's transpilation needs to know what to target as regards modules and JS version, which makes things even crazier than they already are.
- vbg 5y agoRather a negative outlook.
- davnicwil 5y ago> for some completely unclear reason, ESM proponents decided to remove that property. There's just no way anymore to directly combine an import statement with some other JS syntax This is one of those 'worse is better' things in language design, I believe. It guarantees simplicity, traded off against extra verbosity. In fact, when it comes to the common and probably most valuable case of reading and understanding code written by others quickly, it is not even a tradeoff really, as both are good. Whether or not that was one of the driving reasons, it certainly is a benefit in my opinion. The two examples given in the post of an inline require don't demonstrate this well, as they're both really simple. I'd say the benefit isn't to stop things examples like that being written and replace them with two lines of code, which admittedly might sometimes be slightly cumbersome. It's that it stops the long tail of much more complex/unreadable statements being written.
- eyelidlessness 5y ago> This is one of those 'worse is better' things in language design, I believe. It guarantees simplicity, traded off against extra verbosity. And with top-level await the restriction goes away (albeit the ESM equivalent is still a bit more verbose). (await import('anything'))(...yup)
- joepie91_ 5y agoI would have considered this a valid argument if overly-clever use of `require` was actually a problem in JS. But it's not! These 'simple' types of obvious cases are the only types of cases that people actually use this syntax for in practice.
- eyelidlessness 5y agoI have seen horrors of abuse of the CJS require cache, I’m glad to hear you haven’t had to deal with it. For what it’s worth, whether it’s bitten you or not, every single instance of my sillycode[1] is in use in Jest (granted in obviously more useful ways). And it’s an enormous headache to debug when it goes wrong. A trivial example: require a logging library which creates a singleton at module definition time and provides no teardown API (yeah that sounds like a bad design but believe me they exist, are easy to find, and hard to replace on a busy and/or opinionated team). If you have a single suite with 100 tests, Jest will leave 100 instances of that singleton running and consuming memory even while totally idle, completely inaccessible to most any machination you might come up with to try to free them. Which isn’t to say ESM doesn’t have this same problem if you try to bust the import cache with eg query parameters. But at least you’ll probably notice it’s a problem because you’re very probably doing it directly and not with some opaque Babel transform that hijacks the entire module system and any code referencing it. 1: https://news.ycombinator.com/item?id=29140847 https://news.ycombinator.com/item?id=29140847 Edit: forgot which sub thread I was in, added link to my sillycode