TypeScript: .mts file extension does not work as expected (tsc emits commonJS code instead)
Bug Report
I have created a post on stackoverflow asking about this behaviour, I haven’t gotten a single reaction (yet). I asked on stackoverflow first, because I thought I might used tsc wrong.
However, getting no reaction in conjunction with the “obvious” expected behaviour I decided to file a bug report here.
The behaviour is easily reproducable by first installing typescript like so: npm install --save-dev typescript then creating the example.mts file and then running ./node_modules/.bin/tsc example.mts - that’s all.
I expect *.mts files to be convert into VALID *.mjs files WITHOUT using a configuration, I think that’s a reasonable expectation.
🔎 Search Terms
.mts
🕗 Version & Regression Information
- This is the behavior in version 4.9.4.
💻 Code
// file name: example.mts
import path from "path"
console.log(
path.resolve("./")
)
🙁 Actual behavior
// file name: example.mjs
"use strict";
exports.__esModule = true;
var path_1 = require("path");
console.log(path_1["default"].resolve("./"));
This is not a valid .mjs file because ES Modules do not have a require function.
🙂 Expected behavior
// file name: example.mjs
import path from "path"
console.log(
path.resolve("./")
)
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 1
- Comments: 19 (7 by maintainers)
Commits related to this issue
- feat: correct module systems by file extension. addresses microsoft/TypeScript#51990, microsoft/TypeScript#27957, microsoft/TypeScript#50985 — committed to knightedcodemonkey/duel by knightedcodemonkey a year ago
- feat: correct module systems by file extension. (#5) microsoft/TypeScript#51990 microsoft/TypeScript#27957 microsoft/TypeScript#50985 — committed to knightedcodemonkey/duel by knightedcodemonkey a year ago
You still are making arbitrary decisions, just like Node.js, but in contradiction to the latter. M$.
The TypeScript compiler is fundamentally broken in its handling of
--modulecombined with its support for the new file extension.mts.Let’s say you want to create a CJS library, but also have some tooling scripts that are written in ESM, for whatever reason (and there are reasons), that should likewise be part of the build output. For the most part your project uses
.tsextensions for the source files, but the ESM scripts are using.mtsto indicate they are ES modules, regardless of thetypein package.json, because that is after all how Node determines module systems. Well, unfortunately you are out of luck if you want to do this withtscbecause you’d have to pass--module commonjswhich always converts the file with the.mtsextension, to one with an.mjsextension, but using the CJS module system!Moreover, if you were wrapping
tscto create a tool for generating dual packages from an ESM-first project that uses an.mtsextension (for whatever reasons) while also using--module nodenext, and wanted to fix the issue described above by creating a separate, dynamictsconfig.jsonfor the CJS build that changes to--module commonjswhile removing any.m[tj]sfiles so they can be copied from the ESM build, you’re again blocked by the way theexcludeoption works. Yes, you can just overwrite the broken.mjsfiles generated by the latter build, but that’s beside the point.This article on ECMAScript Modules in Node.js should be updated to warn about what’s described here. Particularly this part under New File Extensions:
I would suggest this diff:
It seems the only thing for
tscto do when it encounters the new file extensions, is to leave the module system alone, no matter what--modulevalue is passed. Or add more options, whatever you prefer, but this needs to be corrected iftscwants to produce output that works correctly with Node’s resolution of module systems. For what it’s worth, it seems you can preserve the.ctsextension’s module system.My two cents:
Dual ESM/CJS packages are essential at the moment due to the convoluted degree of support for ESM in the NodeJS world. ie, bundlers need all ESM code for maximum tree shaking, whereas a lot of tooling still requires all CJS to be parse-able. Simply saying “that’s a bad idea” isn’t an acceptable solution. None of this is a good idea, it’s all just the unfortunate reality of where the ecosystem is right now.
That’s right, dual module format emit is not a well-supported scenario. #54593
Yeah, this isn’t really practical for TypeScript. There are just too many variables w.r.t. where the code is going to be run, what ES version is supported by the target environment(s), etc. Sane defaults are great, but there’s only so far that goes before you can no longer make everyone happy and configuration is required.
That said, TS’s defaults really aren’t sane these days - there’s a lot of legacy baggage there (default
targetofes3, anyone?). It’d be great to at least havenode16as the default module mode, now that ES modules are mainstream.moduleshould benode16, and you should leaveesModuleInteropon. It will only affect your emit if you write CommonJS files, so there’s no harm in leaving it on.