 Okay, thanks everyone for coming to my talk on importing modules as ESM from Node.js. Quick who am I? My name is Turing Siren. My pronouns are he and they. I'm a Senior Cloud Developer Advocate at Microsoft. I'm a collaborator on a bunch of things in Node.js, including being chairperson of the Node.js Community Committee. And my Twitter handle is at bit and bang. So how do we use ESM in Node.js right now? Before we get into that, I want to give a quick disclaimer. ESM in Node is experimental. It is unflagged in 12 and 14, but it is still experimental. Some things might change under the hood. Some things might change in the DX. Just want to keep that in mind. You might not want to push it to production yet, but you might want to start thinking about that if it's something you're interested in doing in a suits your use case. So there's two approaches to ESM. Implicit and explicit. Implicit, you add some metadata to your package JSON and you change requires and module.exports to import and export as appropriate. In explicit ESM, that's literally just the file name, so .mjs and .cjs. You probably heard of .mjs. .cjs is a newer addition that's going to come with the development of ESM in Node.js that, you know, mjs indicates ESM, that the file is ESM, cjs indicates that the file is .js. So those are kind of interesting approaches to taking that explicit. Cool. So how do we use ESM implicitly in Node.js? So there's three steps, at least for this simplest example. Add type modules to your package JSON, so that's, you know, the property type with the value modules, flip over to import and export instead of require and module.exports and run your code. Here's an example of what you need to add. So literally just one property and that all files that are close to that will be resolved as ESM. So all .js files will be resolved as ESM until there is another package JSON that has either no type or type comma js. So, you know, you can actually nest different, different package JSONs with different code and they will be able to kind of resolve implicitly according to the nearest package JSON and what that package JSON defines. So here's an example of a comma js file. So you can see it's index.js. So we're implicitly assuming that it's going to be comma js. const dsm equals, you know, an object that has some properties and module.exports.tsm. Here is the ESM version. We're, you know, sprinkling in some ESM. So we're just going to do same const dsm and we're just going to be flipping our export to export default ESM rather than module.exports.tsm. To run this previously for a bit there, we were having to run node with a flag, the experimental modules flag. Now we're just going to be dropping that flag. So this is just going to be, to run this, we're just going to run node founding. So in our case, we were naming that file index.js. So we're going to do node index.js. Take a look at a quick example. Okay, so let's get started with npm init and the dash y shortcut. This autopull of my presets for, you know, my config for initialization. By default, yours will be a bit different, but you can go ahead and change it too. And this goes ahead and sets up our package JSON. We're going to want to update our main to ESM.js rather than index.js. And then we're also going to want to update type to be module. So this word, this is going to be the file we're going to create. So our main file is going to be ESM.js. This is just the file name I chose to use. And then type module is going to indicate to us or indicate to node how to process.js files. So for ESM.js, it's going to actually be processed as an echo script module. So let's go ahead and create that. And for this, our content is going to be the same. So we're going to want to, you know, have a normal object as our output, but how we actually import and export that is going to be different. So for here, I'm going to want to do const ESM. And then that's going to be an object. So this is going to be the object we're exporting in this module. This is going to have three properties runtime, which is going to be process uh, no, not version, not node. So this is going to be the node version that we're currently running. The next one is going to be modules. This is going to be process that versions modules. So this number or this, this property on the process that versions object is a single number that node increases every time the ABI or application binary interface for the modules system changes. So every time there's an addition, every time that substantially changes, that number gets increased by one. And then we're also going to do have, going to have a nice message here. Node loves ESM. Cool. So that's our object. That's, you know, normal JavaScript, how we do it, no matter what. And then we're going to have export default ESM. So our default export for this module, module is going to be ESM. And this is just the name of the object, not saying, ECM script modules is the default export, but just we're exporting this specific object. Cool. So we're going to want to log that though. So we're going to actually want to consume this. So how we're going to want to do this is with log.js. This is just, again, another file that I'm deciding to create. We're going to do import ESM from, quote, ESM.js. We're going to want to make sure we have a path there. So ESM.js. Cool. And then, you know, now that we have this object that we're importing, we're going to want to log that. So console.log ESM. And let's make sure we save our package JSON and then do node log.js. Perfect. So there we're seeing ECM script modules powering a module instead of node that's output outputting the node runtime version, the module API version, and a sweet message. Let's talk about explicit ESM now. So how exactly can we use explicit ESM? For explicit ESM, we simply have to change the file name from index.js or whatever.js to index.mjs or whatever.mjs. The .mjs is the explicit part where explicitly telling the runtime, hey, this file is, in the case of mjs, ESM. The same is true for comma js. So, you know, taking the case of index.js, if we want to explicitly tell node that this file is comma js and should always be run as comma js, we can just change the extension, the file name, to index.cjs. Previously, we would have had to run this with node experimental modules, but again, now we're just going to run this with node, and then in the case of mjs, node index.mjs, and in the case of cjs, node index.cjs. This will tell node that, hey, you know, we're running this with ESM or comma js, run it like that. One thing to note here is that you don't have to explicitly run the file. So, anytime node encounters .mjs or .cjs, it will run those files as if they were exoscript modules or comma js. Let's get into an example of that. Okay, so to get started on moving from implicit ESM to explicit ESM, we're going to want to do a few things. First, we're going to update our main to use a explicit file extension, so esm.mjs, and we're also going to, in this case, you don't always have to do this, but in this case, I want to have .js default to comma js. So in this case, I'm going to remove type module from our packet json. Now, our main export is going to be read as esm, despite being .mjs and despite there being no thing, no type module in the packet json, our export will still be parsed as esm. So we're also going to want to reflect the file to actually make that work. So we're just going to update the file name to esm.mjs. None of our code here actually changes. This is still esm. We're just updating the file. Where our code does change a bit is in log.mjs. So we're actually going to want to update the log, the name of this file too, since we're intending to run this as esm. We're going to want to update our import for that as well. So all that takes is just updating the file name, and we should be good to go. So let's try that out, node log.mjs. Perfect. Let's talk about conditional exports. Specifically, these are a tool that we have available to us that allow us to conditionally export esm and export comma js. Going line by line, first we have main, which is pointing to modules.cjs. This is for older versions of node, and it helps us kind of define an entry point for those versions that they will work with, which in this case is the comma js version. The next line creates a exports property that has an object, or that is equal to an object, and that object has two properties, import and require. Now, this is the most bare bones example we can have of conditional exports, and for the sake of simplicity here, that's what we're going to go with. There's a lot more granularity here that you can kind of use to control this. Definitely recommend taking a look at the docs if you're interested in kind of understanding that more in depth, or feel free to ask in the Q&A, or chat with me at some point. Feel free to reach out however you're comfortable. But specifically the import is exporting, so basically this is saying when someone imports this, export module.js. And the same is true for require, when someone requires this, export module.cjs. And in the case of the import, we're using module.js because of type module at the bottom there. But this kind of allows us to change on demand what we are giving out. So we've already pre-built these files, we already have kind of done this work ourselves, and we're making sure we're returning the right thing for the right use case, which is a super useful feature for a variety of reasons. Specifically, it kind of helps us scope down what's delivered to who, helps with things like tree shaking, and it helps teams deliver a more seamless experience when they're trying to transition from something like Comma.js to ESM. If you want to slowly replace parts of your code based one by one with Comma.js and move them to ESM, this kind of helps simplify that a bit and reduce the immediate need for change and allows you to flip the switch pretty incrementally. Additionally, it kind of helps provide a path for maintainers to provide options for user choice. So I personally have enjoyed Comma.js for years and I'm probably going to be doing more ESM. But for now, there's some cases where I just kind of want to be using Comma.js and that's okay. If I'm using a module that has this property that does conditionally export things, I can kind of go from Comma.js, I can use it as Comma.js, and then eventually, if and when I want to upgrade or change to ESM, I can go ahead and do that and kind of flip that switch. So conditional exports, as I mentioned earlier, are a subset of the package entry points feature. I didn't really name that, but that's kind of what this is kind of a part of. I would highly recommend going and taking more of a look at this. This is in the Node docs in the ECMAScript modules section. It's a pretty complex feature that has a lot of granularity and something that you can really allows you to control how you want to use it. I've only shown a small bit of it here, but I definitely encourage you to go take a peek at it, since the rest of it is relatively comprehensive and allows you to do what you want to do with this. Let's go take a look at a quick example. So for conditional exports to kind of exemplify them, to show them off, we've gone ahead and done a little bit more work on simply the work we were doing before and expanded on it a teeny bit. So we've gone ahead and created a new project called exports, and we moved that work that we were doing previously into the Node modules directory. So you can see here in our new project, if we do CD Node modules and then LS, we have simply SM. So if we go back up in our directory, the only thing there right now is Node modules. And then to actually kind of expand on the work we've already done, we moved ESM back to .js and set up type module in the package JSON. So ESM.js is now going to be parsed as ESM again. And with that, we can kind of take the next step of setting up conditional exports. So let's go ahead and do that by setting up an export property in our package JSON. This is going to be an object. It's going to have two properties. For now, we're going to start with import. And that value is going to be ESM.js. So anytime someone tries to import this module, they're going to be getting ESM.js, which we've defined as type module, so it's going to be parsed as ESM. Now, anytime someone tries to require this module, we're going to want to give them a different file. So we're actually going to set up common.js.cjs. So here we're using explicit ESM or explicit file extensions to say, hey, this is going to be common.js when someone's using this. So with that, we're going to want to go ahead and change or create rather common.js.cjs, which is going to be a basically a copy of ESM.js. So I'm going to go ahead and copy that file. I'm going to rename it to what we want to rename it. So it's going to be common.js.cjs. And we're going to change the property here to common.js. We're also going to change what we're exporting to common.js. And we're going to change how we're exporting it. So this is going to end up being module exports, common.js. Cool. So now our common.js export is going to correctly run this common.js when either tries to actually require this module. So let's go ahead and take a step up into our other project. So in the CLI, we're going to run npm init-y. So this is going to create a package JSON in our higher level project. So if we go ahead and look in there, this is going to default to index.js. We're going to want to also add type module. And then we're going to want to go ahead and actually create an index.js. So since we've added type module, index.js is going to be ESM. So let's go ahead and do index.js. And in this index.js, since it's going to be ESM, we're going to have to use import. And I want to import the messages that are coming from our package in node modules. So I'm going to import ESM from simple ESM. So this is importing what that is going to export for the import case in our package. And then if we go ahead and try to set up logging for that, so .log ESM, we should be able to just go ahead and run this and have it work. So we're going to do node index.js. Boom. There's our ESM use case for conditional exports. Now let's get into how this would work for comma.js. So we're going to do the same thing. We're going to create an index.cjs. So in index.cjs, we're going to want to see a require. So we're going to do const common.js equals require simple ESM. So this specific require is going to be hitting the require export. So we're actually going to end up getting common.js.cjs. And if we just do the same thing of const or console.log common.js, we're going to be able to see that node index.cjs should just work. Boom. So you get the same output for common.js and ESM with just a kind of minor change with conditional exports. Awesome. And with that, I just want to say thanks for joining me. If you have any questions, more than happy to answer them. If you want to reach out to me, you can. Absolutely, I am at bit and bang on Twitter. You feel free to tweet me, DM me, anything like that. Also, I'm on the OpenJS Foundation Slack if you'd like to reach me there. And with that, thanks.