by theprotocol on 11/7/21, 9:03 AM with 165 comments
by xg15 on 11/7/21, 11:23 AM
...
"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.
by spankalee on 11/7/21, 3:25 PM
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.
by aravindet on 11/7/21, 3:29 PM
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)by TekMol on 11/7/21, 12:15 PM
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.
by andrew_ on 11/7/21, 1:22 PM
> 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.
by davnicwil on 11/7/21, 11:51 AM
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.
by Chyzwar on 11/7/21, 11:21 AM
by eyelidlessness on 11/7/21, 4:32 PM
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.
by tbrock on 11/7/21, 11:39 AM
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.
by emersion on 11/7/21, 12:40 PM
by junon on 11/7/21, 12:46 PM
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.
by Aeolun on 11/7/21, 11:02 AM
I tried doing this with one of the new fangled frameworks and seeing my browser work through like 5000ish required files was quite comical.
by bricss on 11/7/21, 11:16 AM
by jitl on 11/7/21, 1:19 PM
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.
by forty on 11/7/21, 12:29 PM
We are using typescript which is using "import" syntax, but as far as I know, it's still transpiling to good old "require".
by dgb23 on 11/7/21, 12:56 PM
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.
by amadeuspagel on 11/7/21, 11:10 AM
That's not true with skypack, right?
by somehnacct3757 on 11/7/21, 12:04 PM
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.
by cryptica on 11/7/21, 12:25 PM
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...
by jokethrowaway on 11/7/21, 11:54 AM
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.
by lampe3 on 11/7/21, 12:36 PM
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 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.
by austincheney on 11/7/21, 12:25 PM
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.
by cookiengineer on 11/7/21, 12:59 PM
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.
by eyelidlessness on 11/8/21, 6:34 AM
by arh68 on 11/7/21, 12:14 PM
by tolmasky on 11/7/21, 12:30 PM
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.
by incrudible on 11/7/21, 12:40 PM
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.
by aurelianito on 11/7/21, 2:42 PM
by terracottage on 11/8/21, 12:49 PM
1 file per thing is midwit code organization strategy for people with no actual sense for it.
by rado on 11/7/21, 12:23 PM
by aliswe on 11/7/21, 10:16 PM
on a personal note, im making a cms with es modules and couldnt be happier.
by lucideer on 11/7/21, 11:03 AM
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
by throwaway2077 on 11/7/21, 12:46 PM
// ...
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?by axismundi on 11/7/21, 12:00 PM
by vbg on 11/7/21, 11:41 AM
by api on 11/7/21, 1:52 PM