Back to overview

Titus Wormer Chats About ECMAScript Modules

Learn about what ECMAScript Modules bring to the table!

It's time to embrace ESM (ECMAScript Module). NodeJS is providing support for ESM out of the box. With ESM modules coming out of the experimental stage we're going to see a lot of packages begin to embrace it.

ESM provides superior organization of your code by allowing you to more easily create smaller, reusable chunks of code. ESM gives you a "module scope" where not only are functions and variables available to each other in the same module but also allows you to explicitly make them available to other modules. There is also the Loader API that is currently in its experimental stages.

In this episode, you'll also learn about more differences between ESM and CommonJS, and some of the challenges and potential problems of using native ESM today.

Guests

Titus Wormer
Titus Wormer

Transcript

Kent C. Dodds (00:00):
Hello friends. This is your friend, Kent C Dodds. And I'm excited to be joined again by my friend, Titus. Say hi, Titus.

Titus Wormer (00:06):
Hi Titus.

Kent C. Dodds (00:07):
Yes, very good. All right. So if you didn't already listen to the first episode that I had with Titus, where we talked about open-source and ASTs, and it was really interesting and fun, definitely listened to that, because that's where you get to know Titus a little bit. We're going to jump right into it and talk about native ESM. That stands for ECMAScript modules. So import, export. This may be the only way that you've ever written JavaScript before, but you may not realize that you're probably compiling those away to either common JS and node or Webpack module system or whatever. But ESM is here. It's super here and we want to figure out how to make our code run with native ESM modules. So, anyway, Titus, can you give us a little intro to like the module space, like why do we have different module formats and stuff like that?

Titus Wormer (01:06):
Yeah. So we're both not experts here, on ESM. So there're much more qualified people, but we both are users of this, a lot. And I, as a maintainer, and you, also as a maintainer and also as an educator, and we are noticing that it's time to really start using ESM. So, the module space... Well, I guess in the beginning, we had script tags, right? And you edit a script tag to your HTML, and probably not everybody is from this time, but some people will remember. And you just added more scripts. And if you wanted to do JQuery and some JQuery plugins, then you had them all in the window namespace, you had those at fair as variables. That worked pretty well, but it's kind of unmaintainable. It's a lot of scripts, and you need to minify all that.
So there wasn't like a real module format. And then [inaudible 00:02:16] came, require JS, which is different than the Require that you know from mode. So it wasn't async function that you could give it a path to a file, it loaded that file. Async and then you got that module as a in-your-cup callback. Then, at the start of... I'm not sure if it was in the first release of Node common JS, but common JS isn't a Node-only thing. There used to be alternatives to Node before, and maybe they still exist. And then is now also, I guess, an alternative to Node. But what really made Node popular is this great module format called common JS and MPM as well, because it was so easy to reuse code. And you used to have snippet folders, but now you can just import it from NPM, which better. But that didn't work in browsers.
So if you want it to use that code on the front end, you need to compile these things away because it wasn't actually JavaScript, it was something in node. Well, and that's okay. So we're now at 2015, I guess, and then the spec game with, "Okay, we're going to do modules and we're going to import and export things." And that's ESM. So the plan for that was made in 2015, and now it's six years later and it's perfect. And it works in most places. And especially on April 30, the last node will be end-of-life on that date. And that means that Node 12 and 14 and... I guess 16 is coming up around that time as well. So all those Node versions will support ESM out of the box. So we're finally here, we're finally at this beautiful moment where ESM works. You don't need to combine it the way... You can, of course... Often it's useful to minify your code, but you don't have to. Yeah. That's the background, I guess.

Kent C. Dodds (04:36):
It's here. Yeah, it totally. So I remember early on when Babel supported ESM at the very start, I think it was Babel five, maybe, that I... I started with 65 actually, and I was compiling and I was happy. And when I upgraded to Babel six, I think, they made a big change with the way that they compiled ESM, and the way that I'd been using it was totally wrong, and all of my stuff was busted. And I'd been teaching people how to use ESM and I've been teaching it wrong. And I just was like, "Oh, no, shoot." And so I wrote this blog post. I'll put it in the links, but it was a inter-operate between common JS and ESM issue there. And so it's been a long and kind of interesting road, but now we have an official way that common JS inner-plays with ESM and vice versa, like how you get ESM modules into common JS modules and how you import common JS modules into ESM modules and stuff like that.
It's all well-defined. Seriously, it has taken a really long time, as you described, but I think that the Node project has just done a phenomenal job of being very thoughtful about how they make this work without totally breaking the community and having a Python 2, Python 3 situation, where you've just got two completely separate communities like, "Does this work in Python 3 yet?" So this is a good and exciting time. So, what do you think are the biggest benefits to using ESM? Why couldn't we have just stayed with common JS in Node?

Titus Wormer (06:23):
Yeah, there definitely are some benefits, but I would have been fine using common JS, I think. It worked well. We, as a community, use one format, and we compiled things and... Yeah, that was just really easy. Of course, it's a six year old format or more for common JS, and now we have a more modern format, so there are some new niceties in that new format. But, indeed, we are now at this crosswalk routes where we have these two things, and how they interrupt is complex. Tree shaking is something that's mentioned a lot as a benefit of ESM, although it's not completely true. You can do some tree shaking on common JS as well. And you can perfectly do tree shaking on ESM either. So it's a bit more nuanced. Well, just the fact that it's all async, that's really nice. And that's what I noticed. Because for folks...
So last episode, I mentioned that I have a lot of projects, and, I think for 30 days now, I've started rewriting projects in ESM. So I think I'm a hundred projects in, and it just feels nice. The format and with the tooling and not having to compile for it to run in a browser. And yeah, it's just... I think it's something that folks need to try out and notice.

Kent C. Dodds (08:03):
Yeah, a hundred projects is a lot, but that's... One thing that you mentioned that is interesting to me is that ESM is async, and I think let's get a little bit more clarity on that. So when I have an import statement at the top of my file, I don't have to write async code to access that. What you're talking about is loading those modules for people who are handling the loading of those modules. That part is async. And you can dive into this a little deeper, but when we're talking about common JS, when you require a file, you can actually kind of slip in between that required statement and actually getting the module with some note APIs. But that code that you write for that, which most people don't write that, but if you're doing something like that, that code has to be synchronous. But what you're saying is with ESM, that code can be async. Is that right?

Titus Wormer (08:58):
Yes. And that's really a great benefit for... Well, one example is in Node, you read files from the file system, but you're on the surface, so that's pretty fast, and that's fine. But in a browser, you can read from that server really fast, right? So that's a whole network request. And that takes some time. So having that be async is really beneficial there. Node doesn't yet allow that import.... Oh, and that goes over HTTP, right? Not a file path or something. It's an HTTP import. Node doesn't have those yet, but they can be added through this mechanism that you just explained. So that also Node could request stuff from it with HTTP URLs. So we talked about MDX, so it's also possible to import MDX files in node and have them be compiled or... Yeah. A lot of stuff like this, [inaudible 00:10:13] components also kind of works like this, so it prevents you from including your server code on the client by throwing an error. Yeah.

Kent C. Dodds (10:24):
Yeah. So this is like... It's called the loader API, right? Where you can add special loaders to Node, to say, "Hey, if you're importing a .MDX file, or even like a .PNG file or whatever, you can insert some code to say, 'Here's how you want to handle it.'" I think more people would be familiar with a Webpack loader or a roll-up plugin or something, right?

Titus Wormer (10:46):
Yeah. and this is a super experimental API in Node that's about to change. So this definitely hasn't crystallized yet, but the fact that this is all possible with ESM, and there are like a couple of other things going on in the node project that are super exciting, that are made possible by ESM. Yeah. That's super cool. But also fairly new.

Kent C. Dodds (11:11):
Yeah. Exciting new things made possible by the async nature of loading these... For us regular people who are just writing the imports and stuff, we don't have to carry about that all kind of implementation details of Node and how it resolves modules. Can you touch on some of the... Especially since you've been migrating things over, what are some of the biggest differences that you've noticed? I'll just mention one and I'd like to hear your thoughts on this. One thing that I've noticed is I can't import a module and rely on the package [inaudible 00:11:50] on like main to have it resolve to the main file or even rely on the .index of a directory. I think there's some extra configuration or flags or something you can use. Can you talk about that a little bit?

Titus Wormer (12:07):
Yeah. So in browsers, ESM is the only thing, right? But it'll Node with the package.json, there you need to switch either: Am I in common JS, or am I in ESM? And you do that by... Well, by default, you are in common JS. And if you add a type property to your package.json, and you set it to the string module, then it becomes a module. There's an alternative way to switch between these things. And that's if you name your files with the .mjs, then it's always module. And if you call it .cjs, then it's always common JS. So, that's how you can mix these.

Kent C. Dodds (12:45):
And, to be clear, that's if you say type module for a package.json, that means every file that's a sibling of that package.json, as well as any file in directories below it, are all going to be assumed to be a type module unless they ended .cjs, right?

Titus Wormer (13:04):
Yeah. But you can also gradually move to ESM by having your normal common JS project and then starting to name some stuff .mjs. And then there are some differences in detail. Well, in Node, you were able to import directory and then it will look for... Or you would be able to import something, require something, and then it'll try a bunch of places. So, okay, is it a directory, then I'll try index JS and then I'll try index json. All these things, very magical. And that's nice. It's a pretty nice experience. But on the other hand, being super explicit is also a nice experience because it's not ambiguous where things are coming from. So by default, ESM... it wants to be explicit. So you need to name your files, or use the imports of those complete paths.
But there is a way also to do interrupt between both ESM and cJS. There is a way where you can now mark files in your package, in your project, as private. And you can define which of those expose through an exports property with an object in your package.json. And there are a lot of possibilities. So one of the things I mentioned earlier that Node is adding, which is pretty cool, is conditions. So you can set, for example, development or production as a condition. And, based on that, it could import certain files, like either this one or that one. React server components is another example that is using conditions. Is it running on the server or is it running as a client. And this is all possible with export maps.

Kent C. Dodds (15:08):
Oh, that's awesome. Yeah. I have looked at that just a little bit. And one thing that I do in lots of my projects is I'll have a couple of /import things. So you can import react testing, or testing library react/pure to skip all of... After each that we add and stuff like that. Or in MDX bundler, I have a /client that you can import for the client side of this stuff. And to make that work, I have to have a file that I put at the root of my project that re-exports what I bundle or my build output in this directory, which I've always hated doing, but it works. And so I just keep doing it that way. But with the package.json on exports, I... Oh, and the other problem with this is just people can import directly from this directory. They can just grab whatever file they want. And then they...

Titus Wormer (16:11):
[crosstalk 00:16:11] API is that you kind of want to change because they're pretty new and it was only private. And they could've used that. And then now you change it, and it's breaking everything. Yeah.

Kent C. Dodds (16:24):
Precisely. And if they need it, then they can ask me, and I can maybe solidify that and then take that into consideration when we make breaking changes and stuff. So having this exports just changes all of that, and doing conditional exports is also very interesting. For React, I know their main export is just a single file. It's four lines that says, "if process no DNV, or processing and be no DNV is production, then module exports require this file, otherwise this other one" for their production development mode. So it seems like the Node team knows what the community is doing, and is making official APIs and configuration for enabling those sorts of use cases.

Titus Wormer (17:12):
Yeah. Yeah. They are working really hard and they're doing awesome stuff.

Kent C. Dodds (17:18):
Yeah. Cool. Once you say type module, everything in there as a module, unless it ends in .cjs. You can add the exports so you can specify which files people can import. And if you don't add the exports, then you won't be able to say package/name of export. You say package/name of file, right? Is that's how that works? So you could just continue doing what you've always been, but there is a module property, right? So Is that a Webpack only thing, or is that like a...

Titus Wormer (17:56):
That's nonstandard. That's a thing that... Well, probably not only Webpack, but probably roll-up as well. So that's, I guess, well, what we had the last years, when ESM wasn't ready, it's where... You could publish both ESM or fake ESM and common JS. And then your users would either get the module one, if they were using WebEx set up to do that, or they were getting the .cjs. But that's not a Node or NPM thing. That's a custom Webpack thing.

Kent C. Dodds (18:35):
Got it. Yeah. So, for Node specifically, you're going to say type is module. And then if you want to make it so people don't have to go directly to the files that they're importing and list out the whole file name, then you have exports. Now there is a way for me to say, "When somebody is doing just import my module or import this directory, this is the file that they get", right? Is that part of the exports?

Titus Wormer (19:02):
Yes. I do have a lot of projects that only have one index.js file, so I'm not too sure about whether directory imports work. I haven't checked that out. I think. Yeah, no, I don't know. I think you needs to have full path in your imports, [inaudible 00:19:24] index.js or whatever. But we have ESM, right. And you can export many more things from that ESM, and you don't need to worry about bundle size because the tree shaking is supposed to be better. So, instead of having a client and the server entry point, you could also have that in one file. And then folks either import sever from your thing or client from your thing, or, indeed, with an export map.

Kent C. Dodds (19:57):
Yeah. Actually, just saying, I have tried some of that, where combining surfer and client files in a single directory, and that mostly works. The biggest problem with that is if Webpack or you spilled or roll up has been configured for client only, then they do have to go through all of the imports, even if that... So let's say I've got a file that's doing some server and client stuff. And I have an import that's only necessary for the server stuff. It will resolve that import and all of its dependencies before the tree shaking actually happens. I don't know if that's a limitation and maybe that's a feature that could be added where it says, "Oh, let me check to see what's going to be tree shaking first. And then I'll remove all that stuff. And then I'll resolve what's left." That would be really nice. If anybody listening wants to work on that, that literally impacted me like last week.
So I'd love to get that to not be a problem. I know that the Remix project is working on sidestepping that by using a special file name convention, where you have a .client or a .server where you can say, "Hey, this import for this file, if it ends in dot server, or if it ends in dot client, it's only needed for that", so you can get rid of it ahead of time. So that seems more like a workaround, but we'll see where that lands too. Cool. Okay. So, if I wanted to start using native ESM today, what are some of the big issues I'm going to come across and how do I avoid those prompts?

Titus Wormer (21:33):
Yeah. Interesting. I think one thing we haven't discussed yet is that it's really hard to use an ESM project, or it's kind of hard to use an ESM project, that's only ESM in your common JS. But the inverse is much easier. So say you have a dependency that updates to ESM and you try to use it in your code, it's going to break. It's not going to work. But on the other hand, if you upgrade your project to ESM, it can use all your common JS or ESM dependencies perfectly. That works well.
Yeah. So that's also... Of course it takes a lot of, or some time to migrate your project to ESM. And, especially right now, this ecosystem is super influx and stuff is changing and some tools don't support it yet, but say that's stabilizing in one to two months, then I think it would definitely be a good time to switch. And if some of your dependency never updated, that's fine because it's left bet or something. It's a super stable module that never changes. It's common JS. It'll work fine in your ESM. What are the problems, I guess, your question was. What are people going to run into? Yeah. So the ecosystem... Electron currently crashes on the imports. So you can't use ESM there. I think they're also going to switch around April 30. I know Jest is also having some difficulties. Well, you also know that Jest... Great to elaborate more by your experience.

Kent C. Dodds (23:29):
Yeah. Yeah. So for me, it's just has experimental support. One thing that's not supported at all yet is jest.mock. So if you use any mocking... Which makes sense, because the way that Jest mocking works right now is they have a Babel plugin that will insert their special mocking code at the top of the module before your require statements, because your imports are turned to require. They can hook into the module system before anything's resolved. But, because now we're using native ESM, they can't do that. They can't make any of your code execute before the imports happen.

Titus Wormer (24:08):
I think this is right away possible with all this new note we just talked about. [crosstalk 00:24:14] note that, I think. Okay, Awesome.

Kent C. Dodds (24:16):
Yeah. Yep. So they're working on that now. But the real problem comes in when you try to combine Jest with native ESM and TypeScript, and that... Right now, the biggest problem there is the way that you integrate a common JS module with a ESM module is by doing a dynamic import. So if I'm trying to import a ESM module, like XDM, for example, that's the example, then I do a dynamic import. And that's fine because the function that I'm using it in is async anyways, so I just async away this XDM package and I get all the exports out of it. But the problem is that, if I'm using TypeScript, then I'm going to compile that down to a required statement, which does not work. So you cannot require a native ESM.
And so then you can say, "Okay, well, TypeScript, I don't want you to compile any imports or exports." And then you say, "Okay, Jest, let's use native ESM. Let's use these flags that you need to use native ESM." But then Node is not happy because you're using ESM in a file that ends in .ts. And you can't do that. You can only use a .js.

Titus Wormer (25:46):
Because you were in a common JS project, right? and you want to use an ESM product.

Kent C. Dodds (25:52):
I guess, at the end of all of this, I'd say the easiest path forward is to convert your project to ESM first. And then you'll be able to make it work a lot better. I'm still working through the best approach to solving this problem. And whether TypeScript is going to work nicely if I use the right combination of configurations and things, but... I really believe that the best way forward is a clean break and just say, "Okay, we're, we're native VSM and let's, let's figure this out now", rather than just trying to make it work for both. Let's just say, "No, if you want to use the old version that was CGS, that's fine. But we're only ESM now", which I think is what you've been doing for lots of your projects.

Titus Wormer (26:42):
I'm making the clean break. Yeah. I was just thinking. We had a couple of Twitter conversations about this publicly. Also, I've had a couple of issues, and some people are really angry that things are changing, and I guess that's always the case, right? It was working perfectly, why change everything? And that's fell it. And then there are also people that are super hopeful that we'll get a new boom of cool tools, and not having to deal with all the legacy that we used to have, but indeed, that clean break. So a lot of emotions, I guess, and a lot of feelings. It's going to be an interesting time.

Kent C. Dodds (27:32):
I'm confident that things are going to be better with native ESM. Things will hopefully work better with the browser. And, just in general, I think that ESM is a better path forward. And whether you're happy or not about ESM versus CGS and you're like CGS was fine or whatever, whether you're happy with that or not is irrelevant. It doesn't matter. I don't care if you're happy with... The fact is that we've got them and what's the best path forward. And I think the best path path forward is just: let's do a clean break, move forward with what we have, and make the most of this. So we've actually got quite a few links here in the show notes. There's one for a blog post by Sindre Sorhus. It's titled "Get Ready for ESM." And this was actually my first realization that "Oh, ESM is coming and PR package authors are going to start only supporting ESM." And this is what you've been doing. Can you kind of talk about a little bit more on why you and other package authors are deciding to do...

Titus Wormer (28:45):
One and not both? Yeah, because we... Well, it depends, but we maintain lots of projects and it's just a lot to maintain different module systems. And my early packages used to be Node, but also Bower JS and also component JS. So we had like not just one package is adjacent, but we had like three of them, and we had like browser bundles in them, and it's just a lot to maintain. Plus the interrupt doesn't work perfectly, so there are cases where... Say you have a module, right? And it supports both ESM or common JS, Yes? But those two are used by two different projects, and those two are using your project. And one is using the ESM one using the common JS. It's just two copies, which is a lot of weight, right? But also those objects are the same and not the same, so you have an array in one and an array in a different one and they're different classes. So there are like these death lock going for problems. [crosstalk 00:30:03]

Kent C. Dodds (30:02):
That's right. I hadn't thought about that. That makes a lot of sense.

Titus Wormer (30:06):
And there's also... Because we had so long of this fo ESM that Babel and Webpack allowed, which is also really useful, but it also means that there were some bugs in them that work differently in Node and in browser. So they had one version and now they're different one. So I also tried publishing one that had dual support, but then a bunch of people are using create React app, which is using WebEx 4, so this broke everyone. Yeah. So I don't have the best experience there. And I also don't want to maintain a complex build steps, et cetera. I just want to write JavaScript and help developers develop. And yeah.

Kent C. Dodds (30:56):
Yeah. I think that's a good call there. I didn't even realize some of those issues. So this is why I typically try to trust people who have experience with stuff. Because they normally have a good reason to back up their opinions and decisions. Cool. Is there anything else that you wanted to bring up about native ESM before we wrap this up and give people their homework.

Titus Wormer (31:23):
It's going to be really cool. It's going to be a bred, new, brave, awesome, cool world. ESM only. Yeah. Ready for it.

Kent C. Dodds (31:33):
I'm ready too. Very cool. All right. So our call to action. I've actually written a blog post titled "Super Simple Start to ESM Modules In the Browser". This is actually written already, but I'm also going to write another blog post to call it "Super Simple Start to ESM Modules In Node JS". And so our homework for you is to follow along with one of those blog posts. Mostly we were focused on node in this episode. And so I think I'd recommend trying that out for Node. And just to give yourself some exposure to that and explore the idea of moving to native ESM for your stuff. Anything to add with that, Titus?

Titus Wormer (32:17):
No. Yeah. And like, I'm not a hundred percent when this episode will air. So I just hope the tools will be more ready than, and yeah. I hope this episode also ages well. Maybe everything went perfect and we're there already. We'll see.

Kent C. Dodds (32:37):
It'll be a fun one for the history books anyway. So. Cool. Hey, thank you, Titus. It's been a pleasure to chat with you. Remind us again, the best place to get in touch with you.

Titus Wormer (32:49):
At WOOORM. So that's worm, triple O, on GitHub or Twitter. If you don't like my tweets, that's fine. Yeah. There. Thanks for having me.

Kent C. Dodds (33:01):
Yeah. Thank you. We'll see you all later.

Sweet episode right?

You will love this one too.

See all episodes

Featured episode

Josh Comeau Chats About Gaining a Reputation

Season 4 Episode 21 — 32:30
Josh Comeau