I've started using ESM in a static website builder too [1]. It works, kinda.
Problems that I encountered:
To get `__dirname`, you have to do an awkward workaround using path and url [3].
For npm modules that implement a path e.g. `@lib/method`, the import of this currently isn't allowed in node without a flag.
For ESM incompatible modules, a createRequire [2] function is available to get the old `require` syntax.
For import statements, the cache cannot be deleted yet [4].
Overall, I'm please to see that nodejs will soon be ESM compatible. All changes that have been made look reasonable and useful to me.
In fact, I now often get the pleasurable "wow that makes sense" feeling when using node. It has has great docs, nice new features like worker_threads and it's productive for me. I'm a fan!
If you decide to work with ESM there's only one issue I have with them in practice: There's no way to remap a "global package identifier" to the entry point (e.g. remap import from "package" to "../path/to/mymodules/package/index.mjs") on the Browser-side.
Node.js has the workaround with the "exports{}" map in the package.json file, but there is no equivalent on the Browser-side, so you constantly have to work with dozens of "../../to/index.mjs" files as wrappers that do nothing more than a lot of "export * from ./some/other/file.mjs" inside them, which is a bit annoying.
As a side note (most people aren't aware of it): All Browsers have support for ESM modules, including Safari; and even WebKit2GTK. [1]
> There's no way to remap a "global package identifier" to the entry point (e.g. remap import from "package" to "../path/to/mymodules/package/index.mjs") on the Browser-side.
I think what you're looking for is `require.resolve` [1].
> For npm modules that implement a path e.g. `@lib/method`, the import of this currently isn't allowed in node without a flag.
FWIW, (and im sure you know this, just pointing it out for others) @lib/method is not a path - the @ at the beginning indicates the package is namespaced under an org. @lib/method is a full and proper regular package, whereas `lib/method` would be pointing to a path within the lib package.
ESM modules are literally only downsides and workarounds, there is absolutely no user benefit at the moment, why would anyone adopt this technology other than a standards board saying "You Should Do It"
I absolutely love ESM modules, pretty much the best thing since sliced bread. Node.js is really botching it, though, with the requirement to use the mjs extension. It makes it very awkward and cumbersome to use the same code in the browser and node.
Then yeah, you’re out of luck. But the vast majority of Node modules use a package.json so I wouldn’t say Node is “really botching it”, they’re just doing what works for the vast majority of users.
Quite the contrary. In the browser, workarounds have always been the other stopgap solutions, like script concatenation, require.js, and bundlers like Webpack.
With ES6 modules, bundlers and transpilers are not mandatory anymore for a large part of modern JS development. This is a good thing, as they were a giant source of complexity.
Set `ESNext` as the value for `module` under `compilerOptions` in your TSConfig will output an ES module every time. What it doesn't support currently is changing the output file extension, if thats what you meant
Right, I remember now. The file extensions weren't correct, but even after renaming the files it does not work because the imports were also missing the correct file extension (which is mandatory in ESM mode in Node.JS). At that point I gave up.
I went down the ESM rabbit hole, after 2 months I decided to stay away. Main reason is/was I want true monorepos in TypeScript with single DRY type definitions (so you need to import them to the FE). This alone is with CommonJS already a bigger endeavor but impossible when mixing in ESM for the frontend parts. Nobody should be blamed here because it's so much legacy, so many systems involved, so yeah.
Frankly - it does pretty much everything I want it to. I can write code in Typescript and have it feel like there's no build step at all.
I can use ESM everywhere - frontend, backend, buildscripts, random tasks, etc - and have no problems.
That said - It's not a legacy codebase, and it's only getting hobby level usage (currently serving a few requests a second), so there might be hurdles I'm just not hitting.
> I can use ESM everywhere - frontend, backend, buildscripts,
Once you mix commonjs and esm and try to import from one to another, it doesnt work anymore. Since major BE libs are commonjs you'll quickly face this situation. It's not related to ts-node but to ts' core. Besides, I find ts-node not that spectacular, it hides too much moving parts/it's too high level, it might be good for starters but nothing I'd use in production.
There's an `import` statement with its custom syntax, which yes, does not allow anything else than a static string literal on the `from` part. But there's also an import() function, which is, well, just a function, so you can pass to it whatever parameter you want. For example a string computed dynamically as in your example. The linked page in my comment has a couple of examples of that function.
I think you're thinking of module bundlers, which need to be able to resolve the import statically in order to do code-splitting etc. There's no such limitation at runtime.
Minor nitpick but it’s actually an important distinction: `import` isn’t technically a function, it’s a different kind of syntax that looks like a function. It’s not a value you can pass as a callback, for example. And this is good, in a way, because it allows for all sorts of static optimization that would otherwise make large scale ESM usage trivially pathological.
I find using ES Modules in Node.js to be full of gotchas and anti-patterns - sticking with CommonJS seems more functional in most cases.
1: With ES Modules, imports are asynchronous.
Synchronous imports are incredibly powerful. You can ensure that your application doesn't block on first requests, but instead on load. Initializing connection pools, setting up caches, etc.
You'll also find quite a few packages in NPM that explicitly depend on require over import just so that they can perform the bindings they have to before things get going (see https://www.npmjs.com/package/honeycomb-beeline as an example).
2: With ES Modules imports are commonly destructured.
I know it makes your code feel lighter to have const { add } = import("whatever"); - but as your files grow larger, and those imports start to become complicated bits for middleware or other features, you're just making it more difficult for future maintainers to figure out what those functions mean. A bit of context never hurt anybody, and for my money I'll be that this:
app.use(check);
is not nearly as useful as
app.use(Validator.check);
The example may be contrived - but 9/10 times I find this makes code better than the alternative.
All of these arguments go out the door for Frontend work - that's a totally different terrain. I think one of the big arguments is that developers want to use the exact same JavaScript on frontend and Node applications - but that's an argument unwilling to admit the fact that they're two very different tasks. :)
A nice middleground on your example is renaming during destructure:
`import { check as ValidatorCheck } from "validator";`
This keeps the context at the expense of a slightly more verbose import.
Although the primary reason I use destructuring is to enable tree-shaking, and that's only really applicable for the frontend. I'm not sure why I'd destructure on the backend.
Modules load and eval asynchronously, because there's no viable way for network connected device like a browser to do it synchronously.
But modules still run in depth-first import order, so there is a perfectly fine way do work before other modules run by putting that init code in a separate module and importing it first.
Your second point applies the same to imports and require.
Imports are asynchronous, but they are still eager, and still run before the code in the module. Furthermore, the bodies of modules will still execute in the order you import them in. Basically all being Asynchronous means is that the interpreter is allowed to use multiple threads to load and parse the referenced modules "out of order", but will still execute them in order.
Thus your whole static import tree will be resolved before the first request comes in, assuming you start the server listening in the body of your main module. If you start it in the body of an imported module, and have other later imports that have start-up task side effects, then yeah you might have some requests come in before those later side effects happen, but why would you structure your code like that? The modules approach generally favors not having side effects in from importing modules, except the program's top level module of course.
Now dynamic imports are fully asynchronous, returning a promise. But even then you can structure your code to ensure they are loaded before your start accepting connections, or you can structure dynamic imports to be lazy and on-demand. This is not much different from CJS where you can also structure any dynamic imports to be either lazy or eager. The main difference is that with modules you need to deal with a promise while with CJS's require you don't.
In my opinion and recent experience working with ESM, the design and tradeoffs are worth the trouble especially if you use a bundler or an optimizing build step, and especially if you prefer named imports/exports.
On the other hand, benefits of ESM on Node are currently conditional on whether you’re running the same code on the browser and want consistent behavior on the server. For server-only projects I honestly think you’re still better off using a build step that compiles to CommonJS. That may change as Deno grows in popularity and interoperability becomes more of a concern, or as other compelling ESM features are introduced (for instance native import maps).
Problems that I encountered:
To get `__dirname`, you have to do an awkward workaround using path and url [3].
For npm modules that implement a path e.g. `@lib/method`, the import of this currently isn't allowed in node without a flag.
For ESM incompatible modules, a createRequire [2] function is available to get the old `require` syntax.
For import statements, the cache cannot be deleted yet [4].
Overall, I'm please to see that nodejs will soon be ESM compatible. All changes that have been made look reasonable and useful to me.
In fact, I now often get the pleasurable "wow that makes sense" feeling when using node. It has has great docs, nice new features like worker_threads and it's productive for me. I'm a fan!
1: https://github.com/TimDaub/kloi
2: https://nodejs.org/api/module.html#module_module_createrequi...
3: https://stackoverflow.com/questions/46745014/alternative-for...
4: https://github.com/nodejs/modules/issues/307
reply