Support .mjs output

Experimental support for ES modules just landed in Node 8.5 (changelog, PR).
Since ES modules have some parsing and semantic differences, Node decided to use the mjs extension for ES modules (while js is for the "script" target and commonjs modules).
The current Typescript version (2.5.2) supports ES modules emission but uses the js extension by default. It means that to use it with Node, a post-compilation step is required to change the extension from js to mjs. This adds undesirable complexity to use native ES modules support with Node.
A solution would be to add a compiler option to output *.mjs files when emitting  ES modules.
Edit (2018-03-22): The propositions below are a bit outdated. I recommend reading the issue to see the progression. See this comment for my current proposition.
Notes:

It is possible that new parse goals will be added in the next versions of the ES spec. Letting the user provide the extension explicitly (as opposed to a boolean switch) would allow to be forward compatible with other new extension.
It should still be possible to emit ES modules with the *.js extension, many tools rely on the js extension.
Another solution would be to have a different extension for the source files: *.mts files would compile to *.mjs, this would be similar to *.tsx and *.jsx.

See #10939 (and #9839, #9551, #7926, #7699 and #9670) and this comment.

@kitsonk
Thanks for posting these link but I am not sure if they are relevant, could you explain them? I found these while checking if the issue was already opened: most of the issues you listed are about input files. This issue is about compiling ts files to mjs instead of js (adding an options to change the compiler output).

Basically those say that TypeScript does not want to concern itself with managing extensions like that, as it is already complicated enough.  In particular the output extensions run afoul of the TypeScript non-goal of:

Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.

If you want .mjs files, it would be best to do something like:
$ npm install renamer -g
$ renamer -regex --find '\.js^' --replace '.mjs' './outDir/**/*.js'

Thank you very much for clarifying your comment.
I understand that any new feature is a burden since it means that it then must be supported for a long time, but I believe that the benefits of the mjs support are worth it: it won't turn tsc in a complex build tool.
The long term goal of the Node team is to allow users to author their code using ES modules without paying a cost due to the commonJs modules and script target. This is one of the reasons why the proposals to require "use module" or an export statement (even an empty one) were dropped in favor of using a new extension. See the Node EPS discussion in https://github.com/nodejs/node-eps/issues/57 and https://github.com/nodejs/node-eps/pull/60. If a post-compilation step to rename the files is still required in two years only because .mjs was introduced later then it will just add another burden to remember (like the BOM or the different line endings). Even if there are some discussions to support ES modules with the .js extension ("module" property), the current position is that it should be used as a fallback for old tooling and .mjs is promoted for new code.
Complex build tools have their place for assets management, dead-code elimination, bundling, minification, etc. These all fall out of scope for TS, but being able to produce runnable code does not. Big projects have their own workflows and use the TS library (either directly or through plugins for other task runners), adding the renaming step is not a big deal for them. On the other hand, a sizeable chunk of projects using Typescript are small/medium sized libraries. These libraries usually only need a single tool: tsc. If they want to support native ES modules, they'll currently have to come up with a command similar to the one posted above by @kitsonk. The problems are that it raises the barrier to entry, may cause inconsistencies and makes the build more expensive.
It raises the barrier to entry because newcomers can no longer simply use tsc -p && node index.mjs (--experimental-modules will no longer be required when the use-case for this issue will be relevant) and run their code to discover Typescript. For existing projects, the "build and run" command is usually already defined as an npm script or gulp/grunt/webpack/whatever task, writing this command (or understanding) will be more difficult: for some persons it will be trivial, for others it'll require a few hours of research. The rate of change in the JS ecosystem is already pretty high, let's try to not worsen it.
Now, a related problem is that it may cause some inconsistencies: different projects will use different ways to rename their file. I discovered renamer thanks to the message above, but I would have written a gulp task otherwise. Someone else would have used a shell command with broken edge cases or rolled their own helper Node script. To be honest, I think that most good projects would have a good implementation but this lack of standard way to deal with file renames when no build system is already there may increase the "barrier to entry" problem.
Finally, once the manual renaming is configured, it still represents an additional cost. I don't have any measures but for medium projects and using Node to rename the files, it may be longer to start the VM than running the file rename. Also: what about --watch? How do I make it play well with my custom command? Do you have to have to watch the build directory to rename as the files are emitted?
So far, most of my arguments were about the build complexity: having it done once in TS is better than having each project coming up with its own solution. There is another important reason to have ES modules working out of the box, and it aligns with the goals of TS:

Align with current and future ECMAScript proposals.
Preserve runtime behavior of all JavaScript code.
ES modules are part of the spec and the TS syntax uses them. These modules have a specified runtime behaviour that cannot be completely fully replicated in commonJs. It affects among other thing circular dependencies, errors, mutation of exported namespace, etc. When setting the module option to commonjs, TS does a pretty good job of generating an output with the expected behaviour but it does not trump using real ES modules. So we are in a situation where we can either set an option and have a good approximation using commonJs but if we want the exact spec and runtime behavior we have to jump through hoops and loops.

One last argument is that even if browsers support native ES modules without requiring .mjs, real-world usage of native ES modules will start server-side with Node because you can control its version. Node is important for the whole ecosystem so it would be damageable to just ignore that it uses .mjs for ES modules. You can't just treat it as a "platform detail" when this platform is used by the majority of the TS audience.
To summarize, here are my main points:

Now that native support exists for ES modules, people will want to use it out-of-the-box.
Node settled on .mjs and hope that it'll gain traction. .mjs is Node-specific, but Node is important
Many libraries only need tsc, requiring a second tool for renaming increases barrier to entry and causes duplicated effort (of varying quality).
Using a post-compilation command leads to higher build times and does not play well with the watch mode.
Using native ES modules if the only way to match the spec exactly, TS should not make it hard to use

Just an Opinion
As a very strong fan of TypeScript, and obviously a user of node (I guess even TypeScript is) I would really like to see some agreement between them on an issue that has had so much disagreements over many years of rational and sometimes awkward discussions.
I would have loved it if node had a transition phase to make common js (the non-standard) become .cjs over a period of two years.
That said, mjs seems to be the current direction, obviously that non-js file extension (either one) itself does not matter, but the ability for TypeScript to provide a way to control this inline as the originator of the transpiled files is something that really falls to TypeScript.
That said (again), mjs as a locked extension is kind of very opinionated, someone must have realized that forcing js was okay, it was already called js, but forcing mjs, come on, even my spelling checker is nagging me on that one. I hope nodejs will become a little more flexible on that one.
** Request **
Please make it possible to specify mjs as a module format that simply means compiling es2015 modules and calling them mjs :)

It's important to note here that .mjs isn't only about file extensions. running node with the --experimental-modules flag also means that ES6 module specifiers must be valid urls, so the extension has to be included in the import statement, e.g. import x from './x.mjs.
If I'm understanding this right, renaming the files isn't a solution, TS would also need to compile module specifiers with the .mjs extension in the ES2015+/mjs mode. We're keen to support newer versions of node by publishing non-downleveled iterators as es2015+ modules for IxJS, so it would be great to get an answer on this soon.

TypeScript already allows you to include a .js extension on your imports (even when you're actually importing .ts TypeScript files). The change needed would be to expand that to also allow .mjs.

It's not about the source files including the extension, it's about the compiled files. If I compile import x from './x' into ES5/CommonJS, TS generates var x = require("./x");. If I compile it to ES2015/ESModules with an .mjs extension, TS needs to emit import x from './x.mjs'. If a library is trying to support both old and new node, with both CommonJS and ESM as .js and .mjs side-by-side, node only imports the mjs file if the extension is included.

There are some potential updates in the works… including a --loader option that would you to specify a file with a resolve hook that will take the specifier and parent module path, then it could handle extension prioritization as needed. It seems to be very flexible but their goal is to make it declarative and not have people overloading the actual loader functions.
In terms of picking mjs over js, that is only the case in the CJS loader system which uses the now legacy Module prototype with it's hooks, the ones that everyone likes override all the time.
In the current --experimental-modules release, the ESM loader is hard coded to '.mjs' or falls back to the CJS loader.
What I've seen so far over the past few days makes me believe that the ESM loading system will take over with pluggable CJS loading (unless opting to use the legacy-modules by flag or based on the main file).
Check out this PR/discussion

@trxcllnt I guess regarding the part about baking the extension right into the import… this applies to import/export everywhere now as part of loader specs.
When ES2015 modules were a spec'd, there were no loader specs, the notion that the extension can change from source file to .js made it a more natural way to go and everyone got too comfortable there. But all loaders (especially web browsers) realized that no-extension means potential security and not to mention load-time drawbacks.
So if TypeScript would continue to work without third-party tooling, it will need to find a strategy to write out standard (not ISO but platform-specific) out-of-the-"compiler"-box projects that will just work in nodejs or browsers at least, and do so without asking the user to mockup some hack to get it to work.
This is just an opinion, but honestly, it feels like it for TypeScript to address.

I'd like to include here that the IANA has an Internet Draft which specifically adds .mjs for COMMON usage that represents the Module goal of ECMAScript . .mjs is not purely a Node.js concern and is even included in an example for browser specs and is supported by a variety MIME DBs already like shared-mime-info.
@daflair can you expand on what

(unless opting to use the legacy-modules by flag or based on the main file).

means?
The next step after that PR is to setup per package loaders to be able to guard against global loader mutation. So, it might be going the direction you think.

When ES2015 modules were a spec'd, there were no loader specs, the notion that the extension can change from source file to .js made it a more natural way to go and everyone got too comfortable there. But all loaders (especially web browsers) realized that no-extension means potential security and not to mention load-time drawbacks.

Interestingly the WHATWG Loader Spec had a very early version in the ECMAScript spec that was removed at the last minute before ES2015! There are interesting other things like the original CommonJS Spec which mandated not to have extensions. We should probably avoid dwelling on the past so much since the different loaders vary so much on these opinions.
Node's EP specced a superset of the WHATWG resolve algorithm that does do various Node idioms like file extension completion. However, the browser has a subset that is safe to use in Node. Still, even with that subset there are problems with dependency trees since "bare" imports are waiting on userland feedback (you can get involved in the hook here), but is mostly left up to intelligent servers and service workers for now.
I'd recommend trying to compile down to the WHATWG compatible specifiers except for bare specifiers for now.

@bmeck what I meant by:

unless opting to use the legacy-modules by flag or based on the main file

I was making an assumption that at some point you will have to use something like --disable-es-modules or --legacy-modules which is really an option that simple does the opposite of --experimental-modules (when this flag is the default behaviour) to resort the current loader system and completely bypass anything related to the new loader (ie legacy applications that simply find ways to be incompatible with this existential change to their eco system).
But now that I dug in a little deeper in your recent PR's this might not be your intent…
So it is best to ask you about your intent here 😉

@daflair I'm still not understanding. ESM support in --experimental-modules completely avoids touching CJS except by overtaking the defaulting of .mjs to CJS. I can't think of a reason to introduce such a flag. Also, it would be a very hard sell to change the default behavior from CJS to ESM or vice versa and I doubt that will ever happen.

Okey, when we get to the point where --experimental-modules is no longer needed, at that point, will the CJS loading system that exists today still remain (mostly) unchanged?
If so, at that point, without any of the above flag and without any other flags like --loader, …etc :
a. Would running with a .mjs entry do what it does today with the flag and the CJS loader will simply pass it to the ESM loader? meaning that all dependencies will be marshalled by the ESM loader (even if it delegates something to the CJS loader).
b. Would running with a .js entry always imply CJS (again no special flags) and this also means that ESM loader is essentially completely idle for the lifespan of the process (assuming there will be no require() support for es-modules in the future).

Okey, when we get to the point where --experimental-modules is no longer needed, at that point, will the CJS loading system that exists today still remain (mostly) unchanged?

Yes, it is unchanged except the .mjs reservation.

a. Would running with a .mjs entry do what it does today with the flag and the CJS loader will simply pass it to the ESM loader? meaning that all dependencies will be marshalled by the ESM loader (even if it delegates something to the CJS loader).

No, the 2 loaders are decoupled except when ESM defers to CJS. Loading via import will always go through the --loader hooks. Loading via require will not go through the --loader hooks. Use import for green code going forward.

b. Would running with a .js entry always imply CJS (again no special flags) and this also means that ESM loader is essentially completely idle for the lifespan of the process (assuming there will be no require() support for es-modules in the future).

Yes .js is CJS. However, you can use import() to get a hold of ESM since that is available in all JS (even eval).

And people wonder why the TypeScript team wants to avoid getting into this area at the moment... 🙄 No it is, yes it isn't, no it is  Even if TypeScript gets further into an extension mangling business it would wait until it was clear that --experimental would be retired as a flag and established as a default with appropriate battle hardened semantics.

@kitsonk

🙄 No it is, yes it isn't, no it is

can you clarify what is unclear in the messaging from Node?

@bmeck so one thing that seems like a major foot-gun is how node doesn't automatically select the mjs files when importing a module with --experimental-modules mode on. I understand the story on importing ESModules from node_modules folder isn't complete yet (should it look for the mjs file path from "module" in package.json? who knows). But automatically selecting the CommonJS form of a module then converting its module.exports object to the default export means that code code that works fine in other ESM environments is broken in node:
// node_modules/ix/Ix.mjs (no default export)
export class Iterable {};

// node_modules/ix/Ix.js:
module.exports = function Iterable() {};

// run with node --experimental-modules some_file.mjs
// works fine
import { default as Ix, Iterable } from 'ix.mjs';
assert(typeof Ix == 'undefined') // true, great
assert(typeof Iterable == 'function') // true, great
// broken
import { default as Ix, Iterable } from 'ix';
assert(typeof Ix == 'undefined') // false?
assert(typeof Iterable == 'function') // false :(

@trxcllnt it is complete and has been laid out in depth in https://github.com/nodejs/node-eps/blob/master/002-es-modules.md

should it look for the mjs file path from "module" in package.json?

no.

But automatically selecting the CommonJS form of a module then converting its module.exports object to the default export means that code code that works fine in other ESM environments is broken in node

Correct, because this code cannot be spec compliant. There is absolutely no way to link it ahead of time since you need to eval to get the shape of module.exports. Emulations using CJS semantics like babel or typescript are not enforcing the shape of a module during Instantiate.
This style of eval based shape detection will never be possible.
In the future there may be a pragma that allows parse time instead of eval time declaration of shape, but that would still be a breaking change.
Simply put:

works fine in other ESM environments is broken in node

Is only true because those environments are not valid ESM

@bmeck I'm afraid I wasn't clear. I'm not saying node shouldn't use the exports from a CommonJS module as the default when --experimental-modules is on.
I'm saying that when I'm run with --experimental-modules and I import a module from node_modules that has both a CommonJS and ESModules version side-by-side, I would expect node to select the ESModule over the CommonJS one.
I have this expectation I'm explicitly running node in ESModules mode and using an ESModule import statement on a package that exports an ESModule.

@trxcllnt it does search for .mjs first. https://github.com/nodejs/node/blob/e00a488731ed333c3cd0952acdfe85729b734fa5/src/module_wrap.cc#L37
Your specifiers are different in meaning and you can see if you use import from 'ix/Ix'; that .mjs is preferred. I assume your package.json is explicitly pointing to the .js but was unsure given the example not providing one.

@bmeck npm install ix to check it out for yourself, but the relevant bits of the package.json are this:
{
  "name": "ix",
  "main": "Ix.js",
  "module": "Ix.mjs"
}

We can't set "main": "Ix.mjs", as that'll throw when not using --experimental-modules. Right now we set "module" to the mjs file because most of the major bundlers (webpack, rollup, etc.) will use the "module" path over "main". But from everything I've read so far, there isn't an ESM analog to CJS's "main" entry in package.json (please correct me if I'm misunderstanding this).

@bmeck and also I want to clarify, you and I are on the same side of this issue. .mjs is just fine with me, as long as things have reasonably normal default behavior. I'm in this thread arguing that TS should generate imports with .mjs extensions, because it seems all ESModule import specifiers are now spec'd to be valid URLs.
Considering the prevalence of importing things from node_modules in node, I was a bit surprised there wasn't clarity on this issue for ESModules, and it also did a bad/unexpected thing when we're explicitly trying to support both CJS and ESM.

Remove the extension from "main". Just "Ix"
On Oct 3, 2017 10:00 PM, "Paul Taylor"<EMAIL_ADDRESS>wrote:
@bmeck https://github.com/bmeck and also I want to clarify, you and I are
on the same side of this issue. .mjs is just fine with me, as long as
things have reasonably normal default behavior. I'm in this thread arguing
that TS should generate imports with .mjs extensions, because it seems
all ESModule import specifiers are now spec'd to be valid URLs.
Considering the prevalence of importing things from node_modules in node, I
was a bit surprised there wasn't clarity on this issue for ESModules, and
it also did a bad/unexpected thing when we're explicitly trying to support
both CJS and ESM.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/18442#issuecomment-334037289,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAOUo4SRH5PQaknAohmUHU330-YDy7-mks5sovTkgaJpZM4PWbkF
.

@bmeck HMMM yeah that sounds like a solution. sorry for hijacking the thread everybody.

Awesome @trxcllnt and @bmeck, I guess we are all really trying to figure things out, so a resolution is always a great addition to a thread.
By the way, many of us nagging at node to support ESM with .js are also nagging a TypeScript to support "module": "mjs" that can run with node directly without a --loader.

are we any closer to convincing you?

Convincing me is irrelevant.  I am just a vocal member of the community, with their own opinions.

@daflair

so those index variants will resolve to mjs if imported and js (cjs) if required when things are stable without any special flags, right?

correct. "main" follows normal resolution. It would check all the file extensions in order with .mjs first.

@daflair yes this worked great, and is now out in<EMAIL_ADDRESS>We have an interesting packaging strategy already in place to accommodate the wide spectrum of node/browser/electron bundling needs and support for Iterable/AsyncIterable (some people have it, others want to down-level), so we it's not hard to rename the es2015/ESM files to .mjs in the build process. Considering we follow a similar strategy in Arrow, I've been wondering if this isn't a strategy that deserves to live in a CLI package so other folks can use it, ala create-react-app (hmu if you're interested in helping w/ that!)

It would check all the file extensions in order with .mjs first.

This works for me! Thanks! 👍
I was able to get my project, copee, to emit CJS and MJS. It works in Node and in the browser!
I also wrote about my findings in this blog post if anyone wants to do the same.

One thing that I should bring to the TypeScript team's attention is https://github.com/babel/babel/pull/6455 since it is dealing with babel emitting a pragma and therefore would be relevant for TypeScript.

Thank you for your post.
I had some time lately to properly experiment with the current mjs implementation. I'll try to write a better summary this week-end. So far I have 2 main issues related to interoperability with commonJs modules (so core libs and most of npm).
I want to have the same .ts file compiling to both .mjs with ESM and .js with cjs. Commonjs modules are wrapped as the single default export for their ESM namespace. As a result, when I import a .js file from .mjs, the semantics of the cjs module are similar to export const default = module.exports;. This kind of wrapping is similar to what SystemJS did and the reason why allowSyntheticDefaultImports was introduced. When enabling this option, I can use import fs from "fs" for .mjs to get the semantics of import * as fs from "fs" in .js, but it's broken in .js ("commonjs") because all the references to fs are replaced by fs.default. I haven't found yet how to get "dual" builds only by tweaking the compiler options.
A second issue is that synthectic default imports are triggered only when the module does not have a default property default already. This breaks cjs modules that actually have a default export. For example exports.default = "foo"; in a cjs module gets exposed to mjs as {default: {default: "foo"}} and import foo from "./foo" returns {default: "foo"} but Typescript infers only "foo".
The scenario for projects without any direct dependency on cjs modules will work great once the emission of mjs extension will be supported. Proper interrop with cjs may require some work.

Thank you @demurgos for the very insightful findings. I've also been exploring interop aspects between c/mjs literally (yes, I opted for vanilla in my newest side project) to if I can write interchangeable code completely separated from linkage (imports/exports) instead of trying to using today's TypeScript to transpile across node's flavours — so focusing more on better practices to decouple functional code from linkage.
The side project was a single file which I wrote as CJS & MJS simultanously making sure that both files executed similarly as I worked on them. I might have cheated a little because it required node modules only (zero-dependencies) and did not even require local linkage since it is one file. This scope made it ideal for a pattern to evolve and I quickly went from two similar implementations to identical code (beyond linkage).
So I found that destructuring which is necessary for node's import fs from 'fs'; is functionally indifferent when working with const fs = require('fs');, so that is a great plus for node for opting default-wrapped exports. The downside of course is that restructuring defies the better practices for promoting static analysis (I guess those tools will still having to carry some of that burden they so cleverly avoided after all). The good news is that TypeScript's language service was able to cascade equally across both files and no inconsistencies stood out, so thats a relief.
This pattern worked for my side project to the point that I eventually can literally copy/paste the entire body of the file (beyond the first few lines of linkage) across both .js and .mjs files — at least for this very limited scope.
A key take away is that until issues like yet-to-be-determined fates of module-scoped fields like module, __filename__, and the likes, and until dynamic imports are locked-in, it's logical for TypeScript's team to hold of on devising the module transformation.
Still, I hope once those aspects are resolved, TypeScript (and all other transpilers out there) really buy into making the awkwardly received "MJS" format.
Check the code here: https://gist.github.com/daflair/638b82842f6d083dbb41607ad737badf

Regarding interop with commonjs, I guess that https://github.com/Microsoft/TypeScript/pull/19675 is worth following. It should allow to get import fs from "fs" to work when compiling both for cjs and es2015.

On that topic... I found two possible solutions:

In tsconfig:allowSyntheticDefaultImports used with module ES2015, then back-transpile to CommonJS using rollup (my work is ESM centric so I didn't fuss with CJSing the output bu ESM works)

Using a loader with dynamicInstantiate: https://github.com/nodejs/node/blob/d21a11dc23d6104b1d03fa2ddc1c808dcaf89c31/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs

On the dynamicInstantiate part, I hear the node team might be pulling the plug on that part, but so far it's been more than reliable, and I did make it a point to share my thoughts on keeping it alive for what its worth.

I'm a little confused that there appears to be no method of achieving this.. so I'd just like to clarify what the options are for my use case before I delve down this rabbit hole:
I want to use ES modules in node and the browser the way they are intended to be used.  No CJS, no extra build tools or transpilation other than tsc.
I would currently import a module in the following manner:
import Logger from './logger';
For this to be a valid ES module import, the .mjs extension needs to be added.
import Logger from './logger.mjs';
But tsc does not like this.  Am I correct in thinking there is no way to get this to compile as expected?  From what I can tell the only option I currently have is to:

Use the old method of importing modules
Rename all the .js files that tsc outputs to .mjs
Run a find and replace on all of these files and add .mjs to any import statement that is found

If that is indeed the case I hope this issue gets resolved swiftly!

@tommitytom if you're targeting ESM in node, you don't have to change anything about the import declarations in your files. Just change all the extensions from .js to .mjs after TS transpiles them and (assuming your entry point is also an .mjs file) node will import .mjs files when it encounters an ESM import declaration.

@tommitytom and others
Since I know it's not easy to get TS and mjs running with all the moving parts, I made an example repo: https://github.com/demurgos/mjts
It demonstrates how to compile the same source code to both commonjs and esm and how to import commonjs or mjs (in this case even hybrid) modules. Note that it uses the experimental ESMInterop option that is not yet merged (https://github.com/Microsoft/TypeScript/pull/19675) so you'll have to do a recursive clone.
This module does not use hybrid modules in the same package but you can do it trivially by emitting to the same output directory.
@tommitytom As you can see, you should not use extensions in your import declarations. The only case when you might needed it is if you have both foo.js and foo.mjs in the same directory and, from an ESM, want to forcefully load the commonjs foo.js instead of the ESM foo.mjs (I use it to get a hold on __dirname or create ESM interfaces).

Thanks for the swift replies!  Where this is fine for node, this will cause issues in the browser, right?  Since from everything I've read, and from my owns tests, you have to supply an extension (js/mjs/whatever) when using ESM modules in the browser.  And in my case I have a massive amount of code that is shared between 2 projects, one which is node based and one which is browser based.

I have to retrieve the source but last I heard the recommended way to work around this is to treat the extension as an implementation detail (such as  .php or .aspx) and configure your server to server to reply to /scripts/my-lib with ./scripts/my-lib.mjs.
This can be done fairly easily if you can control the server but does not work well when you don't (Github pages?). In this case, the reply was to use some build tools but I agree that it is not always ideal. I'm also interested in the possibility to have the extension in the output file. Even if this is useful for .mjs, it is also kinda independent. You can maybe track this issue if you're interested in this.

I have control of the server but I'm constantly using different web servers with this project - including hot reloaders and stuff and don't really want to go down that route as it's way harder to maintain and document for other devs.
I ended up writing a small script that does the find-and-replace as I described above and have my project working in both node and the browser with pure ES modules :)
And I must admit it feels GREAT - it's made me very excited about the future of JS, and getting rid of all the awkward build scripts that we've all become accustomed to with gulp/webpack/grunt etc has been such a breath of fresh air :)
The only issues I'm facing right now are getting dependencies working well in code that is shared between the browser and node - I have a hacky work around for now but I'm sure this will be solved in the future when other people reach these issues

It's been a few weeks: it's time for an update.
First a small info to get it out of the way: Node is working on providing ESM variants of the built-in libs. I don't think there should be any big issue with it but it's nice to have.
Now, the big thing currently is the "mode": "esm" PR. The goal of this PR is to make Node aware of package scopes and treat .js files as ESM or CJS depending on the value of the "mode" property in package.json. It means that adding "mode": "esm" would allow to execute .js files as ESM, so you'd no longer need .mjs in all scenarios.
This flag affects all the files in the package scope: this means that this method does not support mixing cjs with mjs. According to the discussion, the main use-case of .mjs would exactly be to support both modes in the same package.
As such, .mjs would move to a compatibility feature. Top-level application would not need it at all if they wish so, but libraries may still want to use .mjs to provide both cjs compat and good integration with esm.

If this Node PR gets merged, I think that a good way forward for TS would be to add support for .mjs as an "mjsModule" compiler option. It would need to be properly defined, but my general idea would be to have the following behavior:

"compilerOptions": {
  "module": "commonjs"
}

Produces foo.js files with cjs syntax. (this is how it already works today)

"compilerOptions": {
  "module": "es2017"
}

Produces foo.js files with ESM syntax with dynamic imports. (this is how it already works today)

"compilerOptions": {
  "mjsModule": "es2017"
}

Produces foo.mjs files with ESM syntax with dynamic imports. (no .js)

"compilerOptions": {
  "module": "commonjs"
  "mjsModule": "es2017"
}

Produces both foo.cjs with commonjs syntax and foo.mjs files with ESM syntax with dynamic imports. It would support first class error deduplication, watch mode, sourcemaps etc.

All the other compiler options are shared. The mjsModule compiler option should have the same end-result as compiling with module using its value and then renaming to .mjs. The fact that all the other options are shared is intended: Node moves to view .mjs as compatibility trick for the module type/parse goal. I don't think larger changes between .js and .mjs should be encouraged, but you'd still have the option to use two tsconfig.json files (tsc -p tsconfig.cjs.jons && tsc -p tsconfig.esm.json) with the trade-off that error messages may get duplicated.

@demurgos On your last comment.
This still leaves the custom loader hooks behind.

@thetutlage
Could you expand on loader hooks and how you'd like to use them here? I am pretty happy with the syntax currently offered by Node's hooks but I feel that they are unrelated to this issue (compiling TS to mjs). I see them more as a feature that could be used by tools such as ts-node.

You are right, maybe it's for project like ts-node ( not 100% sure ). So here's my use case.
For framework like adonisjs.com, we have a custom dependency resolver, which does couple of more things over CommonJs to resolve a dependency. Till now, it was not part of any standard, since Javascript has no standards around module loading.
With the new loader hooks (part of the spec), we will simply remove all the custom syntax and use that hook instead.
Now everything will work great in Node.js, but I cannot neglect Typescript and it's eco-system, as I know Vscode uses Typescript for intellisense, it will fail, since the logic to resolve a module is not the default one but instead provided via hook.
For example:
import User from 'Models/User'

Now Models can be a directory in your project root, or it can a dynamically registered model by some package. At runtime, when Node will use our hook, we will return the right path, but for intellisense this file doesn't exists.
Another use case is of webpack aliases. They also hack upon the require method, but I can see them also using a cleaner approach (loader hooks) in the near future.

I know you can define paths for webpack aliases today also, but aliases is a very simply use case. Whereas, loader opens up a whole new world of possibilities.
I know Typescript doesn't support anything I explained so far, but I am more curios to know, if they will consider this in the near future.
Also happy to share insights and provide coding help, if I know there is/will some work going on same.

@thetutlage
Sorry for the long delay. Loader hooks, custom resolvers and other mechanisms for dynamic modules are important but I don't think they are specific to the .mjs extension.  Still, I see your point that having a standard way to handle this may make custom resolvers more popular and gives TS to work from.
The problem with these custom resolvers is that they are basically code: I don't think you can describe their behavior declaratively (to pass it as a compiler option).
A solution would be for you to write a build task to dynamically generate the compiler options or declarations for your dynamic modules. This is an approach I use personally: my compiler options are generated and passed directly to the compiler, I just emit tsconfig.json files so my editors understand what's going on. You may also want take a closer look into the compiler API, but the more complex your build process is, the harder it will be to make it play well with editors. (BTW: not just editors, but also collaborators: they need to understand what's going on).
I recommend you to open a dedicated issue to track this problem.

Anyone know alternatives for linting? Using VSCode ideally of course.

@jpike88 Node's ES modules are still experimental and may change. This discussion has some posts providing manual alternatives to generate .mjs files from .ts files. I don't understand what kind of linting you want.

Here are some updates for those following this issue:

Dynamic imports landed on stable Node. I tested it with Node 9.8 but I think it's there since 9.6. There is still no import.meta but it's already pretty good. I haven't played with it much yet. I think that setting "module" to es2017 or esnext should be enough to emit code using it. I don't know what is emitted for older module versions or how type definitions are handled.

Babel has an issue to change its defaults for module types (it's an interesting read). There's a comment by @weswigham about Typescript and .mjs:

In the browser, .js is king (and the default), and in node, tons of effort is (actively) being poured into making .js as close to the default as possible (mode flags, loaders, etc - and still not finalized). I believe it's premature to call .mjs acceptable as the default mode of operation for any transpiler (I know we're holding off on it at typescript because 1. we really dislike .mjs since it'll cause our resolver complexity to bloom considerably and seriously damage the ecosystem (many definitions assume babel-style transparent cjs interop), and 2. there's the possibility that its usage ends up really low and isn't worth supporting if it's not implemented well, given the large existing cjs ecosystem) prior to any implementation that actually uses .mjs shipping without an experimental flag.

https://github.com/babel/babel/pull/7501#issuecomment-370636191

I recommend watching the nodejs/modules repo

I've been watching the node/modules repo for a week, and been reading their progress. It's coming along nicely.
But while I wait, I need a solution. For those working purely in .mjs based projects, all we really need for now is just a clean way to get that .mjs file extension on the output.
Does anyone know a way for TS to emit file extension overrides without having to chain a command to a gulp/npm task, or is that really the only option at this point? I see a few people trying to have a way to deal with it, but I want something as simple as possible which assumes all outputs are to be built as .mjs files.

esm package allows ES2015 modules without .mjs extension, it may be a workaround.

Ok that worked really well. Looks like I'm using that for now, thanks.

After months of waiting and meanwhile loosing all interest in TypeScript (due to obstacles like this) and after spending weeks trying to understand how difficult it would be to strip away TypeScript features from source files JIT because ultimately .d.ts files are a given requirement of a good package... Today I took a five minute break and decided to hack my workspace's tsserver to support ".mjs".
**It took me 5 minutes and it worked ** for a silly 5 minute hack.I could do the following:

The issue is in waiting for all (interop) behavior to be specified, not in any technical implementation hurdle. How a node esm module interacts with a node cjs module (and how we can identify either in the type system) is still being designed; we could be conservative and let you do nothing across module type bounds, but that would be an awful experience. Additionally, the mechanism for detecting the correct module type for a package represented by a .d.ts is nontrivial - we must assume that all declaration files that exist today actually represent cjs, and then introduce some new marker for, eg a .d.mts. Additionally, we don't want to ship something and need to change it in a month or two when behaviors are finalized (and retain support for the guesswork for awhile), so we're watching development closely and when stuff comes to consensus and actually lands (and no, an experimental flag doesn't count), we'll start shipping something; but not before (and we're in the node working group looking at this, so we know it is being worked out). This matters more for us than, eg, Babel because while the emit is unlikely to change much, the typechecking behavior and expected import shape is way more up in the air, which for us can be breaking.

@weswigham... I cannot disagree with you about fully fledged support, but what I think is going unheard by everyone upstream is that people have been waiting since 2015 (not singling anyone specifically) and every time we take a step forward, someone else upstream takes ten steps back.
Those of us who are interested in experimental mjs are compromising because Node.js made this call, then compromising and using --experimental-module flag, so we are completely understanding of the reality and do not expect TypeScript not having a ready strategy for non-experimental support.
So now let's switch perspectives, even after we compromised, we only asked that TypeScript offer a similar --experimental-modules mode (with all manner of disclaimers that would void any legal burdens from you guys) we just want what ever comes naturally or breaks naturally to happen when using this mode.
For us, this is the difference between helping you pinpoint areas that can help you with your progress, versus us having to stop working (experimenting) and instead waste months exploring possible "temporary" tooling to mediate the gaps or look into alternatives.
For at least myself, I did both, and considering all the other upstream stops since ES2015's ratification, and honestly because the extent to which I was depending on TypeScript (because it never left me hanging), this issue right here has been the most detrimental to my productivity by far.
So back then in September or even January, I would have been able to be a good TypeScript citizen by uncovering potentially uncharted issues through experimental Extension.Mjs support. But now that I realize how depending on one tool can be detrimental - unless we are always in agreement - and hopefully with a "true" dialog taking place - leaving me in doubt of ever growing too comfortable with any one tool again - sadly even TypeScript.
Please appreciate that this is as honest and sincere a customer review as you can expect in such a forum.

Node.js ES module support is still experimental and requires a flag. Supporting it is not simple, even import { readFile } from "fs" won't work because Node.js gathers everything into default for CJS modules. I think using esm is the best before it gets stable enough to earn a good support from other tools like TypeScript.

@saschanaz no question, you are absolutely right for use cases where using esm appropriate. The reality of this option is that it is compiling your ESM module to CommonJS. This works great for most mainstream use, but excludes being able to specifically test experimental TypeScript scenarios with real modules, which is the who purpose an experimental stage (where users can actually be contributing in places where developers might not have been looking).

Since my last post (timestamped 4 hours ago now) I cloned TypeScript and did my best to take my experiment from hacking the compiled tsserver.js into the actual source.
This required very few changes than anticipated: (some changes are simply due to whitespace reformat/reflow)
 src/compiler/core.ts                  | 12 ++++++++----
 src/compiler/emitter.ts               |  3 ++-
 src/compiler/moduleNameResolver.ts    |  6 +++---
 src/compiler/program.ts               |  1 +
 src/compiler/resolutionCache.ts       |  2 +-
 src/compiler/types.ts                 |  1 +
 src/services/codefixes/importFixes.ts |  2 +-
 7 files changed, 17 insertions(+), 10 deletions(-)

Then for tests:
 src/harness/harness.ts                         | 18 ++++++++++++++----
 src/harness/projectsRunner.ts                  |  6 +++++-
 src/harness/unittests/reuseProgramStructure.ts |  8 ++++++++
 src/harness/unittests/tsserverProjectSystem.ts |  1 +
 4 files changed, 28 insertions(+), 5 deletions(-)

And testing: (needed a jake baseline-accept after eliminating other errors)
Discovered 103 unittest suites.
Discovering runner-based tests...
Discovered 12150 test files in 347ms.
Starting to run tests using 8 threads...
Batching initial test lists...
Batched into 8 groups with approximate total time of 2m6s in each group. (90.0% of total tests batched)

  [▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬] ✓ 60367 passing (2m31s)

  60367 passing (3m)

Linting: node node_modules/tslint/bin/tslint --project scripts/tslint/tsconfig.json --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish
rm -rf tests/baselines/local/projectOutput/
Linting: node node_modules/tslint/bin/tslint --project src/tsconfig-base.json --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish
✨  Done in 206.69s.

I will need to do some due diligence in the morning then open a PR so others with similar goals can collaborate on this.

The PR passed all tests. Since it is only a PR, the actual built files are compiled into built/local. If you are interested in using it, I am putting together a wrapper package and working on a README here:
https://gist.github.com/smotaal/4f9458cf43cd7a55a12d5c476c15400e

@smotaal
Have you considered implementing a node loader hook, which would load .js files as ES modules?
Here is a working draft (requires node v10.0.0):
// @filename: loader.mjs
export async function resolve(specifier, parentModuleURL, defaultResolver) {
    const resolved = defaultResolver(specifier, parentModuleURL);
    const {url} = resolved;

    if (resolved.format === 'cjs') {
        const {pathname} = new URL(url);

        /**
         * Implement you own criteria here. For instance, you could use a glob pattern:
         * src/**\/*\/*.js - consider all js files in the "src" folder to be ES modules.
         */
        if (pathname.endsWith('.esm.js')) {
            // Tell node to load this module as ES module even though it doesn't
            // have the "mjs" extension.
            return {
                url,
                format: 'esm'
            };
        }
    }

    return resolved;
}

// @filename: foo.esm.js
import bar from './bar.esm'

// @filename: bar.esm.js
export default 42;

// Usage: node --experimental-modules --loader loader.mjs foo.esm.js

This way you can avoid patching TypeScript altogether.

Certainly, but performance and complexity makes it ideal for certain applications early on but not as requirements started to deal with integrations with other packages.

Yeah, it's meant to be used only inside of an application. In case of a library, you'd definitely need to change the extension back to .mjs before publishing.
But anyway, with the former approach, integration with other packages is my biggest concern as well.

The future cannot be soon enough. Thanks for all the hard work on this. It definitely is hard to commit to something when it's "experimental".

I have been needing this functionality for my own projects for quite a while. I had a script that would do the necessary transformations after the output had been generated, but it was a bit brittle, so I decided to fork the typescript package and patch it to support the generation of .mjs files (Github, NPM).
The patch itself was surprisingly easy since I only had to add a compiler option and the new extension type (Commit)
You can find my patched version as ts-mjs on NPM.
With the package installed as a dev dependency, you can now call the patched version of tsc as tsc-mjs and if you can add the compiler option --mjs to emit .mjs files instead of .js files.
If you use the --sourceMap compiler option, this will also emit the corresponding .mjs.map source map for your .mjs file.
If the output target for JSX would be preserve via one of the compiler options, regular JSX code with the regular extension .jsx will be emitted.
For example, if you run
tsc --sourceMap --declaration --target es3 --module commonjs index.ts

this will emit
index.d.ts (the declaration file)
index.js (the CommonJS ES3 module)
index.js.map (the source map for the CommonJS module)

If you run
tsc-mjs --sourceMap --declaration --target esnext --module esnext --mjs index.ts

index.d.ts (the declaration file)
index.mjs (the ES module)
index.mjs.map (the source map for the ES module)

The declaration files that are generated will be identical in both cases.
You can use this to publish hybrid modules by running both commands in succession (or in parallel, since they won't interfere with each other). The output will be:
index.d.ts (the declaration file)
index.js (the CommonJS ES3 module)
index.js.map (the source map for the CommonJS module)
index.mjs (the ES module)
index.mjs.map (the source map for the ES module)

If you have other source files, the corresponding 5 files will be generated for them.
If someone enters your package now via an .mjs file, node will automatically search for .mjs file in all import (or require) statements (and it will fall back to .js should an .mjs file not exist).
In your package.json, you can make the main field "dynamic" by removing the extension of the linked file.
{
    "main": "index",
    "browser": "index.js",
    "module": "index.mjs",
    "types": "index.d.ts"
}

For a real-life example of this being in use, check out my hsluv-ts library (Github, NPM).
(Note: The link to that NPM package will be broken during the next 24 hours, because I made a mistake while publishing, had to unpublish it and now I have to wait for 24 hours to republish it).

This is your daily reminder that .mjs and how cjs/esm interop works in node is unstable and liable to change until it ships unflagged - you should not use it in a production workflow unless you're amenable to significant churn. 💓
We will support it in TS once it's stable - use what you find at your own risk. :D

@weswigham I would like to point out that regardless of what Node.js decides to do it's already possible to use any extension in the browser. Which I think is important.
Where I work most of the code is still primarily classic scripts, and I've been introducing bits of modules in internal tools, but to prevent tooling break from new files that are modules and to minimize any nightmarish configuration .mjs was a natural pick. NOTE that this is primarily browser based code (but there are some node tools too using @std/esm) and I think it would be good for TypeScript to support an extension that's guaranteed to be a module for this reason.

Regarding Typescript and Node's native ESM, there is an issue in nodejs/modules. It's relatively old but it wasn't posted here.
Changing the file extension of the output file is one of the least problems. On the emit side, TS tries to keep the differences minimal between the output using different module types. Node having different mechanism when consuming CJS or ESM may be a hard difference to hide.
The biggest issue though is consuming ESM and properly typechecking the dependencies. The type declaration files are mostly using ES syntax, even if the module is actually written in CJS (for example, it "lies" about available named exports). When using esModuleInterop, Typescript cannot know if the import statement is actually valid (ES namespace or default export). If you throw in dual builds and import-based resolution (for example .js/.mjs or main/module), the situation quickly becomes complicated: the type of the dependency depends on how you consume it.

Any news on it?

@romap0 TC39 proposed an alternative: https://github.com/tc39/proposal-modules-pragma
TypeScript will probably follow TC39 when a consensus happens.

I bit the bullet and used fuse-box/fuse-box, works good

@saschanaz
My understanding is the the module pragma has been rejected.
@jpike88
Isn't fuse-box a bundler? I am not sure how this relevant to TS emitting .mjs (which is mostly a Node concern).
@romap0
We are still waiting for Node to settle stabilize its support. Discussions are advancing on the nodejs/modules repo.

@demurgos It's still in stage 1 proposal list: https://github.com/tc39/proposals#stage-1

I use fuse-box for my server code too (i also noticed it initialises a lot faster than the .mjs method, likely due to sub-optimal linking in Node itself)

It's added to the list 3 months ago so I would say it's not "rejected", as any rejected proposals instead go to "inactive proposals" list.

Sorry to add noise in the thread, maybe people will find this useful:
https://github.com/kevinpollet/typescript-es-modules-node-example/tree/master
the gist of it:

"type": "module", in package.json
in tsconfig.json: "module": "esnext", and "target": "esnext",
run it:

node  --experimental-modules --es-module-specifier-resolution=node dist/server/server.js

Hello,
If someone like me come here, want to use rename but want to fix .mjs.map :
{
  "scripts": {
    // Out is dist/esm
    "build:esm": "tsc -p config/tsconfig.esm.json && npm-s build:esm:rename build:esm:edit-map",
    "build:esm:edit-map": "for FILE in `find dist/esm -type f -name \"*.mjs\"`; do json -I -f $FILE.map -e \"this.file='$(basename -- $FILE)'\"; done",
    "build:esm:rename": "renamer --find .js --replace .mjs 'dist/esm/**/*.js*'",
  },

  "devDependencies": {
    // ...
    "json": "^9.0.6",
    "npm-run-all": "^4.1.5",
    "renamer": "^1.1.3"
  },
}

Note also, that main vs module key in package.json is ignored by nodeJS (ref)

@phil-lgr  that actually works pretty well.  The one place I am stuck though is in SSR projects where we have some code that is pulling in CSS files still (for webpack).  We are using ignore-styles package but that doesn't seem to work with this new system.

Node.js support for unflagged native ESM should land in Node 13.2.0 (probably next week).
Here's the PR for the announcement and details.

@weswigham Are there any updates related to this? Would be greatly appreciated to have this coincide with unflagging if there are updates.

@SomethingSexy Assuming you are interested in running the ESM files in Node.js, you can explore --loader which is still experimental but can help deal with things like CSS.
In most cases, I'd urge people to only do experimental things when experimenting, and in this case it is still true, but CSS files loaded in this context are likely merely noop and no error, right?
If that is the case, I'd recommend exploring it cautiously — see Experimental Loader hooks.

Important — Please note that experimental and/or flagged features are highly discouraged if you are publishing to NPM… etc.

It is fair to mention that browsers have looked into non-ESM imports a bit but backtracked (I believe it was JSON) which is discouraging at least for anyone planning on doing away with bundling and wanting all the mojo.

@SMotaal that is a good idea, I will look into that option for our testing.  Ya in this case these files are noop.

@SomethingSexy Can you update your response with a link to this pattern once/if you have it public.

So node 13.2 has been released with .mjs support without flag: https://github.com/nodejs/node/releases/tag/v13.2.0

Sorry to add noise in the thread, maybe people will find this useful:
https://github.com/kevinpollet/typescript-es-modules-node-example/tree/master
the gist of it:
1. `"type": "module",` in package.json

2. in tsconfig.json: `"module": "esnext",` and `"target": "esnext",`

3. run it:

node  --experimental-modules --es-module-specifier-resolution=node dist/server/server.js

You forgot the "compile it" step giving the exact tsc command line that is required for this to work. It most certainly doesn't work with just tsc as of node 13.3.0 and tsc 3.7.3.

What's the TL;DR for generating .mjs from TypeScript files, now that it's been a couple months since .mjs support was unflagged in Node?
@phil-lgr's tsconfig.json does most of it, but the extension is still .js, and I found that setting "type": "module" in package.json was not necessary.

@dandv i my self use rollup to overcome that limitations it has a typescript plugin a checking and a none checking one it has options to define the output filenames.

Another option is to use tsc for type checking only and to use babel to write proper module ouputs:

Strip Typescript Syntax
Set output file extension
Create valid relative specifiers

Using that approach, you'd still use tsc with emitDeclarationOnly to generate type definitions. But the generated JavaScript will be in proper module files and have fully qualified relative specifiers.
There's still rough edges with this since TypeScript may not properly recognize third party imports from modules but it may be a helpful workaround.

the best solution is to avoid .ts extension and go for .js extension then marking the source as module via adding type: module to a package.json near the module imports or even in the package root
then add JSDOC Annotations and turn checkJs true in tsconfig.json

@frank-dspeed I know, once upon a time when some said use ".ts"… now when it is fair to want to do ".js"… things take time I guess, and projects especially like to keep at it thinking it will be solved upstream, even if it has already.
So for folks on this thread, please research closer, TypeScript is a development tool, ".ts" is the way it was best accomplished at one point, but please don't take anyone's word for it, do your own digging. In the end, TypeScript is not going anywhere, even if you use ".js" you are still getting the best perks it has to offer today.

@SMotaal assert{} was always superior + jsdoc.

runtime type checking for debug
easy to compile it away.

I do not really care for typescript itself at present tsserver is the backbone of vs code that makes it matter not that it is a tool.

@frank-dspeed so I feel a little confused when addressing technical issues, how we sometimes mistake what is absolutely everyone's right to express (ie opinions and preferences) with what we all mutually must not dismiss on account of our own emotions on the latter (ie works for everyone, not just the "selfish" me) — the whole idea of open source looking like social media sadly makes this kind of knee jerk reaction likely for anyone of us…
@SamHH @csvn — it really helps if you articulate where you disagree, please, I am inclined to venture but I do not want to be dismissive, so if the 👎 is not meant to discount the very real pain that comes from how typescript and vscode are coupled, which is really the technical point of relevance here imho, then articulating would really help everyone involved… thanks :)

@SMotaal I disagreed mostly with this statement:

TypeScript is not going anywhere, even if you use ".js" you are still getting the best perks it has to offer today.

I use Typescript in almost all projects I work on, and I always use strict: true.  Whenever I've used .js and JSDoc (e.g. for various smaller scripts) the experience I've had has always been vastly inferior to using .ts. A very subjective note, but I feel readability suffers a lot when you need to add comments inside methods and next to variable assignments.
I do wish Typescript supported .mjs so it was easier to use native ESM with Node v13 for experimenting, but I don't feel like using .js is a great option for anything but very small projects. 🤷♂

I don't see where we disagree honestly, yes, .mjs from .ts and .js, this is fundamentally necessary for the ecosystem, but now even .cjs needs to become factored in the whole process.
@csvn it might add context to add I've used TypeScript for ages, and I know exactly the trade-offs, why I moved to .js files and still use TypeScript even as a compiler.
I hope you can appreciate at least the context of my comment in this thread alone, but I do not think we are arguing differently on the core ask in this op, fair?

@csvn
i found out it works best when defining typescript like interfaces via @typedef then you can use short /** @type {name} */ near the var so it is less verbose. you can also do @type {import('./@types....').name}

Any news on that functionality ?

@TheMrZZ you can expect this to stay around for a longer period of time 2+ Years maybe more. I was trying to do a fast fork to fix that it was not so easy it would take some weeks of fulltime work so we all are doomed.

If anyone is interested on how to configure TypeScript & Node.js 14 to work together, I just made a post on StackOverflow on how to do that here: https://stackoverflow.com/a/61305579/148072
tl;dr

in tsconfig.json:

"allowSyntheticDefaultImports": true
"lib": ["ES2020"]
"module": "ES2020"
"moduleResolution": "node"
"target": "ES2020"

in package.json:

"type": "module"

when importing a local file:

import { a } from './a.js' (yes, .js even if the file is named .ts)

@TheMrZZ See closed PR #35148 for some discussions, difficulties, and plans.

@dandv solution works for node14. I've put a solution together for node12 which using a jscodeshift codemod to transform the typescript esm outputs to include the .mjs in the import path + handle directory paths (and lodash/merge). It's quite easy to customise the codemod if you have some other funky 3rd party npm packages which need changes for esm vs cjs. See notes in the repo: https://github.com/ilanc/fix-mjs-import-paths#how-to-run-the-demo

I recently released a package of build utilities for TypeScript packages, ts-pkg-scripts. It includes the command ts-pkg compile which you can use to emit .mjs files.
Internally, this is just using the TypeScript compiler together with gulp for plumbing the output of the compiler to a different extension. It can also generate the correct source maps if you choose to activate them.
Typically, I'd use ts-pkg compile --es -e .mjs -o dist to output .mjs ES modules during the build step. The package also includes some other other helpers for those who are interested.
Here's the command reference:
Usage: ts-pkg-scripts compile [options] [...files]

Compile the project

Options:
  -c --config <path>                Path to tsconfig.json
  --cjs                             Shorthand for compile to module CommonJS and target ES5
  --compilerOptions <json>          JSON string with tsconfig overrides
  --dts, --declarationOnly          Only generate type declarations
  --es                              Shorthand for compile to module ES2020 and target ES6
  -e --extension --ext <extension>  Set extension of generated files
  -f --files --file <file>          Set input files (default: [])
  -m --module <module>              Set compilation module type
  -o --outDir <path>                Output path for the generated files
  -t --target <target>              Set compilation target
  -h, --help                        display help for command

I'm currently looking to convert some JS to TS code, but this is a blocker.
The correct file extension of modules are .mjs, not .js in Node and it would be very strange to rely on some package.json configuration for this output since that might not even be what you're currently building with TS (and Node support modules based on extensions only, no need to specify "type": "module" in package.json)

+1

@LinusU's reply and the respective SO post is very good to get this topic and get ESM working properly on TS and node side.
For those who need urgently ESM (e.g. for the top-level-awaits) I did a chart of @LinusU post to understand the problem better and as note for the future, maybe it's of any help for others driving by:

TS is broken to the ground.
No, TS is great, the team behind is awesome BUT this was my gut feeling dealing with this mess. I take back back what I wrote in my post before about @LinusU suggestion to get this work. You need more to get this work, see the updated chart below.
The problem is not that we don't get this not to work but the mental energy and all the things we need to remember is a big ask. I mean I could rename myself to a Senior TSConfig.Json Engineer, man, this got to such a rocket science.
It's not like that TS gets out of your way. We tried to create work-arounds but would love to see now some more elegant solution from you guys.
Here the updated chart:

My last chart isn't a real solution either because after the last step casting <any> to the imported package, I lose all type infos all imported modules. And now? Does anyone has a better solution? @LinusU are you there?

Indeed, just casting to any could be a temporary workaround at best.

since @LinusU is not responding: @ahejlsberg @sheetalkamat @DanielRosenwasser this incompatibility to node is a major issue and any work-around is a pain and we'd happy to get your official statement if/how/when this will be fixed.

@desmap in the current nodejs implementation the points are fixed
cjs.js
module.exports = 'string'

test.mjs
import me from './cjs.js'
console.log(me) // result string!

so the default export gets used when u use import without {}

@frank-dspeed thanks! but how do i import libs where i cannot change the export? Eg. if I import from https://github.com/MichalLytek/type-graphql which compiles to commonjs but doesn't use module.exports = ....

Dziękuję za wiadomość. Do 24.08.2020 przebywam na urlopie. W sprawach nie cierpiących zwłoki proszę o kontakt z Markiem Michalskim tel. 605 574 647, email<EMAIL_ADDRESS>Sławomir Osoba
Architekt Back-End
tel. (+48) 32 757 4192
tel. kom. (+48) 609 793 920
<EMAIL_ADDRESS> 		 				 COIG S.A. | ul. Mikołowska 100, 40-065 Katowice |<EMAIL_ADDRESS>| www.coig.pl
		Sąd Rejonowy Katowice-Wschód w Katowicach
		VIII Wydział Gospodarczy Krajowego Rejestru Sądowego KRS<PHONE_NUMBER>,
		Kapitał zakładowy w kwocie: 5.400.000 zł, w całości wniesiony. NIP:<PHONE_NUMBER>

Dokument ten zawiera informacje poufne, które mogą być również objęte tajemnicą służbową. Jest on przeznaczony do wyłącznego użytku adresata.
Jeśli nie są Państwo jego adresatem, to wszelkie rozpowszechnianie, dystrybucja, reprodukcja, kopiowanie, publikacja lub wykorzystanie tego dokumentu lub informacji jest zabronione. Jeśli otrzymali Państwo tą wiadomość przez pomyłkę, prosimy o bezzwłoczne skontaktowanie się z nami oraz usunięcie wiadomości z Państwa komputera.
This e-mail may contain confidential and/or privileged information. If you are not the intended recipient (or have received this e-mail by mistake) please notify the sender immediately and destroy this email. Any unauthorized copying, disclosure or distribution of the material in this e-mail is strictly forbidden.

@desmap<EMAIL_ADDRESS>is packaged for use in bundlers only do you see the esmodule export? That only works with babel it is not designed to get consumed without additional transpiling via babel

https://github.com/microsoft/TypeScript/pull/40068#issuecomment-674380499 Is Working on preserve file extension so when the input does spec 'my.mjs' even when it is ts or something else we should get .mjs output needs testing but is WiP

Coming back to this I have been smashing my head against the wall trying to get TS + Jest + Node to work (importing something with .js breaks Jest) and I'm currently using extensionless imports with --es-module-specifier-resolution=node and type: "module", essentially everything is fighting each other and barely staying together.
As I understand it the TS team doesn't want to add any extensions automatically, it's just that.. that is exactly what happens when emitting CommonJS so there's no consistency here.

I has some problem when using intellicode
https://github.com/MicrosoftDocs/intellicode/issues/241

Kindly bump this up please. I would love to use ES modules import syntax after transpilation and this is the main thing which is preventing that. I wonder how Deno is doing this though. Since they support ES module syntax and anyways have to internally transpile ts to mjs or something else.
My question at this point is. if Deno can do it with TS, why is this not possible with TS in node? Should we add something on top to make this possible?

@tvvignesh Deno uses https://github.com/swc-project/swc tool instead of TypeScript compiler if I am not mistaken.

@yordis Interesting. Didn't know that. Is it completely compatible with Typescript compiler? Sounds convincing.

Just saw this: https://github.com/denoland/deno/issues/5432 - Looks like there are still some things to do.

@tvvignesh deno is using parcel bundler internaly that is what gives it TS support everything gets packaged with parcel on load and then runned inside v8
https://parceljs.org/

Feels like commonjs is the IE of node.
I want to output my code to support commonjs and modules, while preserving the directory hierarchy. Think:
import {} from 'rxjs';
import {} from 'rxjs/operators';

To do this I need the following directory structure to exist inside node_modules
/node_modules
  /my-example-package
    /foo
      foo.cjs
      foo.mjs
      foo.d.ts
    index.cjs
    inde.mjs
    index.d.ts
    package.json

Where the package.json looks like
{
  "name": "my-example-package",
  "version": "1.0.0",
  "types": "./index.d.ts",
  "main": "./index.cjs",
  "module": "./index.mjs",
  "require": "./index.cjs",
  "exports": {
    ".": {
      "module": "./index.mjs",
      "require": "./index.cjs"
    },
    "./foo": {
      "module": "./foo/index.mjs",
      "require": "./foo.index.cjs"
    }
  }
}

In the end, I would like consumers to be able to import my package via my-example-package or my-example-package/foo. I intend to have several top level folders for different purposes.
As of now TypeScript does not allow the transformation of the import statements of files to automatically include .js. TypeScript also won't compile if you add .mjs as an import extension so you can't even emit files and post process them yourself with shell scripts.

@alshdavid , after coming to some understanding of rollup and you're configuration, I ended up generating a somewhat simpler configuration that might be generally useful...
// REPO_DIR/rollup.config.js
// setup: `npm i rollup @rollup/plugin-typescript` or `npm i rollup rollup-plugin-typescript2` (for visible TS error output)

// ref: <https://devhints.io/rollup>

// import typescript from '@rollup/plugin-typescript';
import typescript from 'rollup-plugin-typescript2';

export default [
  // ES module build (replaces broken basic TypeScript compilation)
  // * ref: <https://github.com/microsoft/TypeScript/issues/18442> , <https://github.com/alshdavid/rxjs/blob/main/rollup.config.js#L10>
  // * ref: <https://github.com/microsoft/TypeScript/pull/35148>
  // * ref: <https://github.com/microsoft/TypeScript/issues/37582>
  {
    preserveModules: true, // or `false` to bundle as a single file
    input: ['src/index.ts'],
    output: [{ dir: 'build/esm', format: 'esm', entryFileNames: '[name].mjs' }],
    plugins: [typescript({ tsconfig: './tsconfig/tsconfig.esm.json' })],
  },
];

I'm using 'rollup-plugin-typescript2' as the rollup TypeScript plugin here because it surfaces TypeScript compiler error messages, but the "standard" '@rollup/plugin-typescript' works, as well.
Using this configuration, I just replaced tsc -p tsconfig/tsconfig.esm.json with rollup -c as the build script for the ESM module, and now I have a fully-working ESM module with the correct '.mjs' extensions.
Thanks for the inspiration!

Four years later, tsc still can't add a single character to output filenames? You need an external program to work with standard Javascript? This is absurd.

Can someone elaborate as to why this is not being implemented?

@Droppers it gets implemented but is low priority and blocked by some api changes.

Something that makes working with typescript a continual headache and making it almost impossible to keep typescript projects simple and quick to build is your idea of "low priority"?
This almost feels like Microsoft not wanting to create better interoperability between typescript and JavaScript for the purpose of pushing typescript adoption. "It's so hard to use Typescript within a JavaScript project, maybe I'll just switch to Typescript completely". This is what it feels like you're aiming for with these small QoL changes being blocked for years.

Just for the record, the ECMAScript modules story for node has changed substantially since this issue was created. While there's much more in the details, in summary:

.js files will be loaded as either "module" or "commonjs" based on the settings in the package's package.json. To use modules in your project, simply set "type": "module".
.mjs files will always be loaded as "module"
.cjs files will always be loaded as "commonjs"

Especially given that modules are part of the javascript specification it's probably a good practice to get in the habit of expecting import and export statements in .js files.
I don't believe this feature request is even necessary anymore. Instead, make sure tsc is generating output with module syntax (set "module": "es2020" in your tsconfig.json) and set "type": "module" in your package.json. If you happen to have other files that are not generated by TypeScript, rename them .cjs. TypeScript is already doing what it's supposed to do by generating valid output, and we already have standard mechanisms for configuring the runtime that doesn't require the tool to be aware of node-specific naming conventions.

@mike-marcacci this issue is about not being able to output files with the .mjs extension rather than .js, instead you have to rename the .js files yourself using another script, this also makes working with the watch function difficult.
As far as I know when setting the type to module it requires you to have .mjs as extension.

@mike-marcacci this is incorrect. type: "module" is NOT required to use modules in your project; the only effect it has is to change .js from the normal meaning, CJS files, to be "ESM files". .mjs always works, with or without "type" in package.json.
I strongly suggest avoiding use of "type: module".

Use of type: "module" can also problematic with mono-repos (multiple package.json files, unified build tooling). Anything to reduce friction in this area is welcome, as I can say from experience dealing with this precise issue can be a major timesuck.

Hi @Droppers I do understand the intent of this issue – I'm suggesting that it's not relevant anymore, because specifying "type": "module" is a better way (IMO) to instruct node to follow the newer module spec, which allows you to continue using the .js extension as output by tsc. That is, you can probably already do everything today that you want to do, even without this feature. (Check out node's docs on determining the module system.)

@ljharb you are 100% correct in how it works (and I'm pretty sure you and I wrote exactly the same thing). However, we've drawn different conclusions: I'm suggesting that the existence of this node feature, combined with the fact that .js is the standard extension for files that follow the language specified by ECMAScript, renders this issue moot. I can't think of a realistic scenario that would compel a team to use the .mjs file extension as part of a project's SOP. It makes sense as a migration step, but not as a desired end state.

@danielnaab what issues exist with mono-repos and unified build tools that would be solved by changing file extensions? (I ask this as someone using monorepos with where different packages target different module systems.) When specifying the module system at the package level, each package can specify its type separately and supports totally sensible cross-package interoperability.
I DEFINITELY agree that ESM for node has been a disaster of a rollout, especially with all the changes in direction that have happened. For me, the use of "type": "module" and the corresponding idea that each "package" is the barrier for module system selection is the first strategy that's provided a path forward without amplifying complexity. A lot of pain points remain, but of the ones I've encountered, none would be solved by using the .mjs file extension.

WORKAROUND
For folks just seeing this issue, I just want to be clear: you can run TypeScript-generated ES modules TODAY, without any external tooling.

Make sure TS is configured to output files with the module syntax (set "module": "es2020" in your tsconfig.json)
Select the "module" module system for use in your node package (set "type": "module" in your package.json)
If you have any other JS files that use "commonjs", either update them to use ES modules, or rename them to have the .cjs extension

This eliminates all the intermediate hacks and allows you to use ES modules straightaway. Now, of course, there are still plenty of gotchas with ES modules in node (so many that it still may be prudent to continue using commonjs for now)... but those are true regardless of the file extension used.

@mike-marcacci .mjs is and should be the desired end state for ESM files.

@mike-marcacci I'm interested in the "each package can specify its type separately and supports totally sensible cross-package interoperability" part. I have a lot of legacy CJS code and some funky webpack-ed lambda functions. Each .ts library package currently build to both /build/cjs and /build/esm using separate tsconfigs. I then used .exports in the package.json to allow the esm user modules to link to the esm library code and .main to let the older user modules link to the cjs library code.
This was a workaorund that I came up with a while ago and a lot has changed (in node land) and our workspace. So this may no longer be necessary. Before I wade back into that though can I check:

Do you have both CJS modules and ESM modules successfully using a single TS package that has been build as an ESM module with .js extensions using your trick above?
What about user modules which are bundled - especially webpack-ed?

FWICR webpack and rollup did interesting things depending on the package.type=module setting so I had to leave it out.

Hey @ilanc! To answer your questions: in this project we use yarn workspaces; each package has its own package.json and tsconfig.json.

Each generic, shared library-type package builds either ESM or CJS (but not both), and has the corresponding type set.
Currently, all node-executed packages build ESM and do not build CJS (although we have tested it successfully with CJS)
We have one fairly large package that uses webpack to build a react app; it currently builds two versions:

one targets the browser environment, and its files are served as static files
another targets the server environment, and currently builds CJS; accordingly the whole package is marked with type=commonjs; these are actually run by separate "server" package that uses ESM, all without issue.

We do have a few .mjs files in type=commonjs packages (mostly dev tools) and we also have a number of .cjs files on type=module packages (mostly ones that are generated by internal tools or just haven't been updated yet).
I feel fairly strongly that using extensions to configure the runtime is an excellent hack to aid in migration, but a foolish long-term solution. I have a good number of reasons for this, but I want to avoid derailing this issue more than I already have.
Regardless, the current state of modules in node is far from perfect, but much better than it has been, and is totally workable. When working with a bunch of different build tools and runtimes, there's an inevitable amount of fiddling required... and of course, there's also nothing wrong with commonjs in node... but if you feel inclined to begin moving towards ESM, the migration path is increasingly sane.
Best luck!

pm2 does not support ".js".
pm2 does not suport "type":"module".
pm2 requires ".mjs"

pm2 does, in fact, support .js files with “type”: “module”, but it’s a little buggy.  You can find details and some workarounds at the bottom of https://github.com/Unitech/pm2/issues/4540.

*.mjs should go away once node.js fully is on board with ESM. All other JS/ES runtimes use ESM. Node.js needs to get complete the transition.

@btakita it shouldn't, it can't, and it won't. CJS will be around forever, and node will have two module systems for effectively ever.

@ljharb Sure, node.js will never leave CJS behind completely, but effectively CJS usage is already being reduced. Most people who use CJS only use it because the have to, not because they want to.

Perhaps that’s true, but they’ll have to for the foreseeable future whether they want to or not.
The best path forward is embracing this, while incentivizing ESM usage via better tooling - not fighting it by browbeating maintainers, forgoing back compat, or exiling huge swaths of the ecosystem.

My two cents, the solution is not NodeJS specific, but TypeScript. As @ljharb said, the best path forward is embracing this, and wasting our time talking about the NodeJS future is contra-productive and pointless.
I am not sure what I can do to support the TypeScript team, but definitely would be amazing to have such a feature to accelerate the progression in the ecosystem without having to rely on adding Babel + TypeScript in projects or some sort of workaround.
Please give us some love!

The problem is if Typescript can't output MJS and use Export Maps, people are more likely to drop Typescript and the perceived extra weight of its transpilation step in order to move forward, you already see how popular the "no build" movement is becoming.

I believe someone has a PR in support of this much needed feature: https://github.com/weswigham/TypeScript/pull/67

@mike-marcacci this is incorrect. type: "module" is NOT required to use modules in your project; the only effect it has is to change .js from the normal meaning, CJS files, to be "ESM files". .mjs always works, with or without "type" in package.json.
I strongly suggest avoiding use of "type: module".

@mike-marcacci .mjs is and should be the desired end state for ESM files.

I'm don't think these opinions that something "should be" are really helpful for people who are deciding what to use. Ultimately Node will support both "type": "module" and .mjs/.cjs and all it's existing behaviour when neither is used probably for it's entire life.

change .js from the normal meaning, CJS files

It's also worth noting that there is no "normal meaning" for .js, the majority of servers serve .js files to the web as text/javascript which is actually a DIFFERENT syntax target to CommonJS, in particular CJS is actually a function body, so return 3 is perfectly permissible at top level in CJS, but shipping the same file to web it would be behave completely differently (and vice versa).
In fact there's even more interpretations of .js from even that, a short list of prevalent ones:

source code written in ESM that is compiled (and maybe bundled) to multiple targets with differing correctness regarding ESM semantics e.g.:

compiled to CJS violating ESM semantics in significant ways (e.g. binding/evaluation order)
compiled to quasi-ESM-quasi-CJS, e.g. __esModule output in Babel, module.exports = { default: $$autoName } but otherwise ESM semantics preserved
compiled to browser IIFE with either prementioned CJS semantics, or compiled such that ESM semantics are correctly compiled so that it's not detectable it's not ESM

AMD modules, generally denoted .amd.js but this isn't required, different environments have different ways of loading these modules so they're not simply "script" goals (e.g. eval might be used, or <script> tags)
UMD, which allows running as both CJS or AMD or IIFE (and sometimes SystemJS) with differing semantics depending how its loaded, generally denoted .umd.js but again this isn't required
SystemJS, another module system that was designed based on a historical loader proposal for ESM, however has since evolved into a newer system, generally denoted with .system.js but again this isn't required
any of the above combined with various proposed JS features to be compiled with a tool such as Babel
quite a few less known historical ones that occasionally float around (e.g. YUI module system, older versions of things like meteor.js, etc etc)
many many many many many many hand-rolled legacy systems (and even the odd non-legacy ones)

I understand your sentiment but ultimately either choice has tradeoffs, for existing codebases the tooling changes to transform .js -> .mjs may be significant especially if there is fragile tooling that requires .js, or servers that can't handle .mjs, etc etc.
For new codebases .mjs may be more practicable, but even then many tools (like TypeScript) don't support it so it might be a non-starter for a while. In order to being transitioning in probably the next year at least one would need to transition through "type": "module" anyway. Transitioning through "type": "module" to only have another transition period to .mjs seems rather annoying, especially if .mjs doesn't actually add significant value.
And I'm not saying .mjs doesn't add any value, certainly extensions meaning one thing would be nice for understandability, but given extensions aren't even a universal signal (again the web ignores them entirely) it doesn't seem worth the cost especially if projects are wanting to transition soonish.

@Jamesernator browsers don't get a vote as to the normal meaning of file extensions, since they offer no opinion about it - they only care about MIME types. The normal meaning of .js for things that care about it is defined almost solely by node.
I recognize that a ton of things that aren't CJS live in .js files, but that's always been just a convenience. Those files might as well have had no extension at all.

@Jamesernator browsers don't get a vote as to the normal meaning of file extensions, since they offer no opinion about it - they only care about MIME types. The normal meaning of .js for things that care about it is defined almost solely by node.
I recognize that a ton of things that aren't CJS live in .js files, but that's always been just a convenience. Those files might as well have had no extension at all.

Browsers might not but servers certainly do, many many many servers across many languages have not updated their MIME types to map .mjs to .js, so in this regard Node has no more say on the interpretation of .js than servers.
Also this isn't really about "a vote", there is no committee deciding what extensions mean, despite IANA's file associations and all that, anyone can use any file extension at any time for any reason. Certainly IANA and similar aren't even remotely comprehensive for all the file types that exist in the wild.
Ultimately what extensions "mean" is what we let them mean, because meaning is ascribed by people collectively, .js means many things because most people (familiar with it) understand that .js could be "browser code", "node code", "babel compiled source code", "whatever-JScript-on-windows-is code", etc etc

I recognize that a ton of things that aren't CJS live in .js files, but that's always been just a convenience. Those files might as well have had no extension at all.

Yes and no, again servers still have to serve those things, and <script> predates Node.js by years, even in the earliest days people were calling <script> files .js. Signficant amounts of even non-programmers know vaguely about "JavaScript" because browsers give options to enable/disable it. They may even sometimes see erraneous error traces on sites with things containing .js in them, the "JavaScript" they have seen is definitely not the CJS of Node, it's the "script goal" of browsers. Certainly for the average person who only vaguely knows what a .js is would probably say it's browser code.
Even Node does not entirely determine the meaning of .js within itself, even before --experimental-loader and "type": "module", one could override the behaviour of .js with require hooks. Tools like @babel/register, or esm have seen decent popularity, so there's clearly a fairly common understanding that .js can be ESM as well as CJS.

This is all true, but is a great argument to avoid further conflating “what .js might mean”.

i personally want this feature so i can actually use typescript for cloudflare workers (which only accepts .mjs files rather than js for the time being - for the features i need, at least)

watch-able workaround
esbuild --watch config/rollup.config.ts --format=esm --outfile=config/rollup.config.mjs

Notice: Don not pass --bundle flag, else esbuild is no more transpiler but bundler

If you're publishing a library, and looking for a practical workaround, this is a novel solution which involves compiling to ./dist/cjs and ./dist/mjs, and then adding a dummy package.json to each of those build folders that just has a "type": declaration so node will treat the contents of the folder correctly:
https://www.sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html
This author also had an excellent suggestion about how to fix this in node (since really, this is a node.js problem, and node.js is where this should be fixed rather than in every language that compiles to js):
// hypothetical package.json (don't use, not supported by Node):
{
    "exports": {
        "import": {
            "path": "./dist/mjs/index.js",
            "type": "module"
        },
        "require": {
            "path": "./dist/cjs/index.js",
            "type": "commonjs"
        }
    }
}

Also, note if you're looking to rename files to .mjs using something like renamer suggested at the start of this thread, note that you'll run into problems here if you try to import with file extensions:
import myModule from './myModule.js';

Since, of course, the .js file will have been renamed to .mjs or .cjs.  And, if you don't include extensions, you're not really writing ESM, since ESM requires you use the full filename, with extension.

For back compat for pre-exports node, you’d always want to use the package.json approach and the .js extension for CJS files.

Also, node’s ESM only requires the extension for relative paths. Via exports (and self-import), you can (and always should) ensure the extension is omitted.

Also, node’s ESM only requires the extension for relative paths. Via exports (and self-import), you can (and always should) ensure the extension is omitted.

I'm not sure what you mean here.
If I have a library that contains an "index.ts" that does import "./foo.js", this is a relative import, so I need the file extension.  If I compile this to a .js file, then rename it to a .cjs/.mjs file, the extension will now be wrong.  I need some kind of post-processor that will update the imports for me.
If I'm importing a module (e.g. immer) then I just need import "immer", because node.js or webpack or whatever is going to resolve that for me, and then it's fine.
You could probably solve this by running your package through something like rollup - that's what immer does (although immer generates a .esm.js and a .cjs.js - it still names everything ".js").

@jwalton yes, but if that library named "package" has "./foo.js" in its "exports" under "./foo", then it can import "#package/foo" and omit the extension.

you can (and always should) ensure the extension is omitted.

Hard disagree. Ideally a statically served project can be run by proper JavaScript runtimes like browsers and Deno; the import paths should directly map to the file structure. The package exports field should only be used for ESM/CJS interoperability (until we all migrate all packages to pure ESM), not for rewiring deep import paths for aesthetic reasons.
If you do rewire deep import paths, legacy Node.js and dev tools that don't support the package exports field won't be able to resolve the require/import. Also, quite a few JavaScript CDNs don't know to use the package exports field when serving packages:

https://github.com/mjackson/unpkg/issues/265
https://github.com/jsdelivr/jsdelivr/issues/18215

Eventually all server and client JavaScript projects will be ESM, using vanilla HTTP/S imports. node_modules and package.json will be a thing of the past. We should be making our npm packages as closely aligned to this future as possible, to make migrations easier and reduce the work CDNs such as https://esm.sh and https://cdn.skypack.dev have to do to convert npm packages to vanilla ESM in the meantime.

I don't think you're grasping the main point. Yes, Node.js specific tools should should keep up with Node.js APIs (all the ones I publish do), but as an industry we are migrating away from Node.js to vanilla ESM in browsers and on the server, with Deno.
In Deno work I'm writing JS right now without using a single dev tool from npm. Next generation Deno based tools are not "dying on the vine". When I have to use an npm package (mainly react), it's via https://cdn.skypack.dev and it feels a bit gross. We're looking forward to using nothing from npm at all. And I say this as an author of complex npm packages (supporting CJS/ESM via package exports fields and .mjs BTW) that get millions of installs per week. Node.js doesn't excite me anymore.
JS !== Node.js. TS !== Node.js. Node.js is just one runtime of many. It might be hard to imagine a future where we do everything with Deno, but it's fast approaching. Deno already has almost as many stars on GitHub as Node.js accumulated over a decade. Deno improves more each month than Node.js improves each year; the velocity and enthusiasm is incredible.

I don't agree that the industry is in fact migrating away from node, in any significant way, nor that deno is superior or a successor to node. Obviously if this were true, then your thesis would be viable.

@ljharb Also there is a thing that we call "Isolates" We directly use in the industrie at 100k+ Server Directly the v8 C api to run our ESM Code.

@ljharb Node is not the entire JS ecosystem, most JS developers are targeting the browser (even if they use Node tools like npm, they are still targeting the browser).
 That's the polar opposite of being inclusive.
Also, the Node team disagrees with you, because after several years they did finally compromise and add in "type": "module", despite your insistence that they shouldn't. You do not speak for Node, and you definitely do not speak for all JS developers.

Imagine talking with gate keepers for so long about including a single character which would help a large part of the community, despite claiming "This JavaScript is clean, simple code which runs anywhere JavaScript runs: In a browser, on Node.JS or in your apps" on the front page.

@Pauan i'm not "we" there. and yes, the modules group did add type module, and as a result it's caused lots of confusion, and continues to be a mistake. The import algorithm does not, in fact, match what the browser does, because perfect interop with the browser is not possible because node is not a browser.

Can we tone down the political / opinion pieces in the comments here. Stay on topic / relevant to supporting the feature and/or problems if the feature is shipped.

Ohhh!
I think, at least to me, there is already is a solution to both https://github.com/microsoft/TypeScript/issues/18442 and https://github.com/microsoft/TypeScript/issues/27379,
if you are open minded to add a new dependency.
npx esbuild ./config/*config.ts  \
    --tsconfig=./config/tsconfig.esm.json \
    --outdir=./config-out \
    --format=esm \
    --out-extension:.js=.mjs

Adjust directory to your needs.

@ljharb at present type: module is the holy gral as it allows to work with typescript and nodejs using allowJs and checkJs options in the ts config.
the only problem is that it acts on directory level but that is total ok at last it allows successfull migrating code from cjs to esm. Without package type module we would need to add // @ts-check to every file at the top or use a patched typescript version that handels  cjs and mjs as it would handle .js

@somebody1234 Not sure if you solved this—but anyone using Cloudflare Workers can add the following to your wrangler.toml:
[[build.upload.rules]]
type = "ESModule"
globs = ["**/*.js"]

Works like a charm. There’s more info on getting the specific config right in their docs.

Fixed by the addition of module: node12 with #45884 - .mts files produce .mjs files.

BTW, it is zero config.

Yeah, you just type the whole config on the command line.

@weswigham but is it possible to have a single codebase of .ts modules compiled both to .js and .mjs only by changing some settings?
@huw I tried rebuild and it almost works but even if I produce .mjs output files, the imports are still .js (or bare modules if not explicitly imported as .js). Do you know a way around that?

Nope! And I have no intention to make that possible - it is very useful that there's a reliable 1:1 correspondence between input file extension (.ts,.mts,.cts) and output file extension (.js,.mjs,.cjs). It means we can always reliably look up the correct input/output path pairs without needing any configuration.

That's a shame. In that case can you offer any suggestion for publishing dual packages from single codebase?

Running two tsc commands with slightly different configs?

This is not the first issue where all this extension thing is causing confusion.
This will blow up as more people will start moving to ESM.

Running two tsc commands with slightly different configs?

This is your best bet, but best of luck writing code that actually behaves the same under both targets - it's a minefield and I wouldn't recommend it to anyone.

Running two tsc commands with slightly different configs?

This is your best bet, but best of luck writing code that actually behaves the same under both targets - it's a minefield and I wouldn't recommend it to anyone.

by that you mean, TypeScript by itself won't ever support dual target?

@ljharb well, sure but, to the best of my knowledge, there is no tsc config where one can write TS modules as usual and output .cjs/.mjs. Also with imports adjusted for type: module, so that import paths also include the extensions. My current workflow is tsc for one target and babel for the other target

best of luck writing code that actually behaves the same under both targets - it's a minefield and I wouldn't recommend it to anyone.

@weswigham not at all. the only real difference is CJS vs ESM. I have been doing this with babel (and both ways, where either CJS or ESM is the default) successfully for more than a year now. it's a shame I have to resort to babel

@pabloszx well, with node finally supporting ESM and many packages dropping CJS altogether we may hopefully not need dual packages for much longer

That's a shame. In that case can you offer any suggestion for publishing dual packages from single codebase?

And, to further elaborate: I'd avoid it, if it all possible. Probably just ship cjs unless you have a good reason not to. If you wanna expose an esm API, then, ship a cjs core with a handwritten esm wrapper, rather than duplicating all your shipped implementation code. If you wanna do something that only works in esm (eg, top level await) but still provide something for cjs users, then you'd write an esm core with a janky async cjs wrapper (and only if you begrudge your cjs consumers and want to watch the world burn). A "dual mode" package that ships transpiled copies of the same input code is a massive antipattern and the node docs highlight some downsides to that approach (singleton hazards, code duplication within runtime). You can semi-justify it when providing a version for pre-esm (so node 11 and older...) node - but at that point you're shipping cjs still, so you should just only ship cjs, imo.

by that you mean, TypeScript by itself won't ever support dual target?

What I mean is that the set of code that behaves exactly the same as both esm and cjs only seems large - until you realize that anything with an import or require of some kind changes entirely and unpredictably, and so pretty much all nontrivial projects can't cleanly be esm/cjs cross-compiled (unless you're a webpack-esque bundle-the-world type tool). It's the tradeoff we got when node got conditional exports for an interop strategy without the ability to synchronously load esm from cjs - you can't make esm standin for cjs, and cjs can't pretend importing esm actually works (because it doesn't).

Like "just add extensions to my imports for me" as some people seem to think would be sufficient to bridge the gap isn't even actually good enough under current node resolution rules - any people relying on a babel transformer for that will likely be bitten by differences in conditional exports eventually as export maps proliferate. (Export maps make it so module specifiers don't have to correlate with what's on disk at all - in fact, a file exposed under one condition set may well be wholly unreachable in another one.) This actually already caused a headache for preact and react when they first tried to add export maps (since their dependents already used a mix of specifier styles, making any removals a break).

@weswigham i agree with your opinion the only thing that i would change is that i would suggest shipping esm only and transpil that to cjs is a no brainer if needed as it is already static analyzeable.

In Heft's implementation we adopt the mindset that all code shall be authored as ESM first and foremost, but we additionally emit CJS for use by Jest (we still use 25, which doesn't support ESM) or other tools that only use require(). Putting an ESM wrapper around CJS or shipping only CJS makes your library very obnoxious to consume from modern bundlers, since it historically means tree shaking is impossible (though that may be improving), so we prefer to have two completely independent trees.
Currently none of the tools in our ecosystem support conditional exports, so when we start upgrading things are going to get weird. Will probably need to figure out how to drop CommonJS altogether, which will probably involve fixing a lot of unit tests that depend on the way TypeScript emits CommonJS (exports are mutable) instead of the ESM spec (exports are accessors). Also going to have to fix a ton of bad esModuleInterop interactions.

@dmichon-msft tree-shaking has been identically possible with CJS as with ESM for many years; it's just that popular bundlers don't implement it by default (eg https://github.com/browserify/common-shakeify).
Both module formats have a static and a dynamic form, and both formats' static forms are precisely as statically analyzable.

Unfortunately a lot of developers like the export * from './Foo' in modules, and while the ESM form of that can be shaken because there is only one way to write it, the CJS version can't. If not dealing with barrel modules, generally tree shaking is much less necessary to begin with, though still useful. I may look into it to see how that affects outputs.

Absolutely the best approach is deep imports - ie, "only bring in what you need" - tree-shaking is just a best-effort hack to try to reclaim space from wastefully broad importing.

Well ... I guess it is too late, but not even IntelliJ IDE by default recognize .mts and .mtsx ... I have been there, and I am not sure how many tools I am willing to discover that they will break due to such decision.
Any thoughts in the subject matter from your experience adopting this feature?

@yordis no idea about intellij but file extension associations are pretty much trivial to configure on vscode (since they're just there in the lower right corner)

@yordis this future will only be in typescript 4.6 it got droped from the 4.5 time schedule https://github.com/microsoft/TypeScript/issues/46452#issuecomment-960454410

adding .mts etc doesn't really help at all for package maintainers who want to provide .mjs and .cjs exports to be consumed by either audience :(
'they should use a bundler' is probably the practical answer, but there's a space between 'massive package' and 'single file package' that is underserved by that answer.

adding .mts etc doesn't really help at all for package maintainers who want to provide .mjs and .cjs exports to be consumed by either audience

Node is the only platform which cares about using the mjs (or cjs) extension (optionally). For anywhere else, just using .js is fine. In node, you should almost never compile your package into both formats; this causes two copies of your package (potentially) to get loaded into node at runtime, which, in addition to being performance inefficient, can cause issues with state, class nominality, and unique symbols. Instead, in node, you should really just provide a format-specific convenience wrapper around a single source-of-truth package format (eg, a handwritten esm wrapper that imports the cjs core). Ergo, you really shouldn't need to compile the same source to both a .js cjs file and a .mjs esm file - doing so likely means you're quietly breaking something in node. (Now, if you're building for node and another platform, that's another story, but then you don't need the mjs extension anymore, since it's only used for format disambiguation in node.)
Worst case, if you really really badly want to make node load two full, separate copies of your library, you could always compile once into a lib/cjs dir and once into a lib/esm dir, and drop a package.json with "type": "module" into lib/esm. Still don't actually need .mjs at all, let alone output extension overrides for it. It is actually incredibly valuable to know that .ts always maps to .js - it just makes resolution logic so much more easy and reliable.

Hm, if you can just have a package.json with just  {"type":"module"} inside that doesn't break things in weird and wonderful new ways then that neatly solves any objections I have..  the situation I'm thinking about is indeed the package maintainers that need to build 3 versions of the package to distribute on npm because they want to support consumption from node cjs, node esm, webpack, roll-up,

@jamie-pate thats exactly my observation i am researcher in that fild and member of the nodejs package-maintainance group. We Really did break evolution at some points. But the Good thing is when everything would be offered ESM Only with .js extension even without a package.json  we would make the whole ecosystem healty again.

Trying to give a practical example here: I have been working on a deep dependency in our code that has been written in CommonJs a long time ago. I used it as a test to see if I could update it to be a package that is written in Typescript and supports both to be required as cjs (to not break when I drop it in the greater application) and to offer esm for the whole system to be step-by-step updated to ESM-only code. → @tradle/typeforce
My plan was to use tsc to compile teach variant of the src folder into a cjs and mjs folder with source-maps for better debugging experiences.
|- cjs
|  \- index.js
|- mjs
|  \- index.js
\- src
   \- index.ts

In the package.json I defined both the main and module field with the respective .js file. Using two different tsconfig.json variants (1, 2) as previously suggested with a shared base file.
In order to be able to test the generated files I wanted to be able to load them directly in the CLI. import('./mjs/index.js') will fail with the obvious (node:12398)  Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension. error message. So using a script I fixed this to rename all .js files to .mjs. This script needed to be expanded as with the change to .mjs also I better change the type definitions to .d.mts and the source maps to .mjs.map for consistency. The ties between the source and the source-maps had both to be updated as well. A pretty interesting problem that I found was that a .mjs file can not import a relative .mjs file.
(node:12628) UnhandledPromiseRejectionWarning: Error [ERR_UNSUPPORTED_DIR_IMPORT]
Of course in typescript I can not change import { compile } from './compile' to from './compile.ts as I would be running into ts(2691): An import path cannot end with a '.ts' extension. so I had to add some additional fixture in the rename script.
The initial tests were written with tape and to run the tests I wanted to be able to use just run the tests using ts-node test/index.ts. In order for this to work I set the ./tsconfig.json to have module = commonjs as ts-node doesn't support ESM yet(?).
To make sure that all code works same I originally I tried to put the tests in src/test and compile it for each variant and run them in that variant but I havn't managed to compile the tests with the external commonjs libary (fresh-tape) as module = nodejs, which is why I put it in the test folder.
I think it would help quite a bit if typescript would build node esm modules with a .mjs ending and also replace the directory import statements with direct .mjs links.

In order to be able to test the generated files I wanted to be able to load them directly in the CLI. import('./mjs/index.js') will fail with the obvious (node:12398) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension

@martinheidegger check out how https://github.com/jwalton/solox does this.  The build process for solox is very similar to what you proposed with an esm tsconfig.json and a cjs tsconfig.json.  These get used to compile the source into ./dist/esm and ./dist/cjs.  There's a post-build step which adds a package.json to each of these folders; in ./dist/esm we write a package.json that contains {"type": "module"}, and in the cjs folder it's {"type": "commonjs"}.  Conditional exports in the top level package.json ensure that the correct version is picked up based on whether the module is being imported or required.
The result is that all files are ".js" files, but if you clone and build this project you'll find that await import('./dist/esm/index.js') from the node REPL works exactly as you'd expect, as does require('./dist/cjs/index.js').  No rename scripts are required.
My tests are in /test, but they import from ../src, and tests run fine out of the test folder using jest and ts-jest.  (If you run into problems with tests, you might want to flip things so the tsconfig.json is commonjs, and have a tsconfig.esm.json that's "module": "es6".)
Note that one potential downside of this approach (and your original approach) is that it's possible for projects to end up with two copies of your code in memory (if they both import your code and require it in different places).  This approach should definitely be avoided if you have singleton instances or other global state.

Thank you @jwalton for sharing notes.

There's a post-build step which adds a package.json to each of these folders; in ./dist/esm we write a package.json that contains {"type": "module"}

Oh! Thank you for pointing this out. This will indeed make the problem a lot simpler.

. Conditional exports in the top level package.json ensure that the correct version is picked up based on whether the module is being imported or required.

Actually, I also was under the requirement to support deep imports like import * from 'typeforce/async' as part of backwards compatibility and I needed to conditional exports per file. (manual task atm.) It would be nice to have a good automation for that as well.
In a simple experiment with solox (found here https://github.com/martinheidegger/solox/commit/e0a5eddd7389e708ced6686ef0f4147fa6478934) I tried to add a simple dependency and ran into a catch-22 where lint would break without a .ts declaration and compile now breaks with. 😅

In a simple experiment with solox (found here https://github.com/martinheidegger/solox/commit/e0a5eddd7389e708ced6686ef0f4147fa6478934) I tried to add a simple dependency and ran into a catch-22 where lint would break without a .ts declaration and compile now breaks with. 😅

This is a idiosyncracy of typescript, and it's not very intuative, but try making this change:
-import foo from './test.ts';
+import foo from './test.js';

(i.e. change the "ts" to a "js" in your import) and your experiment should build and run correctly.

For deep imports in nodejs you can use the exports property of package.json
https://nodejs.org/api/packages.html#exports
    "exports": {
        "./*": "./dist/*.js",
        "./*/*": "./dist/*/*.js"
    },

NodeJS does not recognize the "modules" property, only bundlers will recognize that.

(i.e. change the "ts" to a "js" in your import) and your experiment should build and run correctly.

Interesting. This seems to be not yet supported by ts-node, though this is actively being worked on https://github.com/TypeStrong/ts-node/pull/1361 I will revisit this and post an update using yours and @jamie-pate input when that landed.

I use swc to transform my ts files to mjs.
swc next.config.ts -o next.config.mjs -C module.type=es6 -C sourceMaps=inline

In order to be able to test the generated files I wanted to be able to load them directly in the CLI. import('./mjs/index.js') will fail with the obvious (node:12398) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension

@martinheidegger check out how https://github.com/jwalton/solox does this. The build process for solox is very similar to what you proposed with an esm tsconfig.json and a cjs tsconfig.json. These get used to compile the source into ./dist/esm and ./dist/cjs. There's a post-build step which adds a package.json to each of these folders; in ./dist/esm we write a package.json that contains {"type": "module"}, and in the cjs folder it's {"type": "commonjs"}. Conditional exports in the top level package.json ensure that the correct version is picked up based on whether the module is being imported or required.
The result is that all files are ".js" files, but if you clone and build this project you'll find that await import('./dist/esm/index.js') from the node REPL works exactly as you'd expect, as does require('./dist/cjs/index.js'). No rename scripts are required.
My tests are in /test, but they import from ../src, and tests run fine out of the test folder using jest and ts-jest. (If you run into problems with tests, you might want to flip things so the tsconfig.json is commonjs, and have a tsconfig.esm.json that's "module": "es6".)
Note that one potential downside of this approach (and your original approach) is that it's possible for projects to end up with two copies of your code in memory (if they both import your code and require it in different places). This approach should definitely be avoided if you have singleton instances or other global state.

@jwalton This proposal looks like a duplicate of probably the most insightful post on the topic to date cc @mobsense : https://www.sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html
By the way, for those who may be interested in having a look at the rewrite strategy path, it also seems worth considering the 2 packages below :
• tsc-multi
• tsukuru

@gfortaine Excellent suggestion. By placing a { "type": "module" } into my /dist/esm folder I no longer have to have the "type": "module" at my root package.json but my ESM is now properly importing, assuming I manually specify the .js extensions, but this is close.

tsconfig-to-dual-package cc @azu