Repository navigation
Apply same ESM/CJS interop in .mts/.ts files as in .mjs/.js files #17288
Description
Feature request
Webpack changes the behavior of imports of CJS files with __esModule to be more compatible with Node when the importing file extension is .mjs, or .js in scope of a package.json with "type": "module":
// @Filename: dep.js
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = "default";
// @Filename: main1.js
import dep from "./dep.js";
console.log("from .js", dep);
// @Filename: main2.mjs
import dep from "./dep.js";
console.log("from .mjs", dep);from .js default
from .mjs { [__esModule], default }
For TypeScript to offer checking behavior that reflects this, the behavior of .ts files needs to match the behavior of .js files, and the behavior of .mts files needs to match the behavior of .mjs files. However, Webpack doesn’t know what these extensions are, so they don’t get considered for Webpack’s Node-like ESM/CJS interop layer. If I add main3.mts with identical content, I get:
from .js default
from .mjs { [__esModule], default }
from .mts default
The fact that a default import behavior is different in a .mjs and .mts file makes it impossible for TypeScript to implement a module checking mode for Webpack.
What is the expected behavior?
- The behavior of an import in Webpack does not change when renaming the importing file extension between
.tsand.js,.mtsand.mjs, or.ctsand.cjs. - Module syntax and interop restrictions placed on
.js/.mjsfiles based on their extension or package.json"type"are also applied to their TypeScript counterparts. For example, whenever it’s an error torequirean ESM file or to useimport/exportsyntax in a file in Webpack, that error should persist when renaming the file to its equivalent TypeScript file extension. (This is less important to us than (1).)
What is motivation or use case for adding/changing the behavior?
I’m trying to implement better support for bundlers in TypeScript, and this behavior of differentiating module interop based on file extension, but not including TS file extensions in that differentiation, is a blocker. (I can explain why if necessary, but I don’t think it’s super relevant here.) We’re tracking at microsoft/TypeScript#54102
How should this be implemented in your opinion?
I’m not a Webpack expert, but it seems like there could be advantages to letting the loader API set the module format:
- No TypeScript-specific carve-outs need to be implemented in Webpack core
- Other non-TS loaders that consume non-JS file extensions could adopt this behavior if desired
- Webpack could expose the new API in a SemVer minor, while ts-loader et al. could adopt it in a SemVer major, which would probably be easier for users to adopt than waiting for a new Webpack major version
The goal for us would be that TypeScript/Webpack users get consistent behavior between .ts/.js files by default in the near future.
Are you willing to work on this yourself?
Happy to discuss solutions, and can potentially put up a PR with guidance if necessary
Additional info
I forked Tobias’s interop-test repo while researching this issue, and have changed quite a bit to show how various bundlers handle TS extensions and what they do with this issue in particular. Notably, esbuild exhibits the behavior we’re advocating for. Take a look at https://andrewbranch.github.io/interop-test/#synthesizing-default-exports-for-cjs-modules if interested.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
alexander-akait commentedon May 30, 2023
Sorry, we can't, it was dicussed a lot of time and you can found a lot of issues about it (in 2020, 2021, 2022), people still have mix of esm and commonjs code, it will be big breaking change for webpack v5
andrewbranch commentedon May 30, 2023
This is not possible, because the TypeScript-specific file extensions I’m referring to were first created one year ago, in 2022.
Can you say more about this? It sounds like what I’m proposing, but I couldn’t find anything about it in the docs. https://webpack.js.org/guides/ecma-script-modules/#flagging-modules-as-esm doesn’t give much info on how a loader would interact with this.
alexander-akait commentedon May 30, 2023
I mean other developers ask this question (not you 😄 )
output:
andrewbranch commentedon May 30, 2023
Yeah, that doesn’t look like something we can expect every user to set up in their own ts-loader config. Also, it doesn’t seem like it allows the
typefor a.tsfile to be set based on its package.json context. User-land config may be an interim solution, but at some point we would expect webpack + [any TypeScript loader] to implement an interop behavior that is possible to type check without additional configuration.alexander-akait commentedon May 30, 2023
@andrewbranch esbuild and webpack do the same:
output:
webpack
output:
I think without additional options on typescript level it is impossible to solve, look at other bundlers, they have the same problem and provide option(s) to change logic (some of them are broken by default 😄 ), so developers can setup any configuration and you will not work out of the box.
Webpack knows nothing about ts-loader (other loaders). So you can ask about it
ts-loaderdevelopers. We internally discussed about changing it before release webpack v5, but when checking popular repositories at that time, it broke almost everything, now I think the situation is much better, but unfortunately still not good enough.Technically you are right and I agree that
"type": "module"should set"javascript/esm"everywhere, we can start warning about potential problems and then, in future, enable it by default (I think we have an issue about it)andrewbranch commentedon May 30, 2023
This is very much not what I’m proposing. What we need is for
.tsfiles to act like.jsfiles and.mtsfiles to act like.mjsfiles. That’s all.I am the person trying to make additional options in TypeScript; that’s why I opened this issue 😄 Additional options are impossible for us to make for bundlers that can’t treat TS extensions the same as JS extensions.
Please see https://andrewbranch.github.io/interop-test/#synthesizing-default-exports-for-cjs-modules again—esbuild differs between
.mjsand.jsfiles like Webpack; I think this is good 👍. But esbuild also treats.mtsand.mjsfiles the same (good 👍), where Webpack treats them differently (bad 👎). That’s why esbuild gets the 💙 in the table and Webpack does not. My goal here is to get Webpack a 💙.alexander-akait commentedon May 30, 2023
Hm, can you provide an example with webpack configuration and code? I may be confused because it should work identically, if not, I want to say it is a bug on ts-loader side, but I want to investigate
alexander-akait commentedon May 30, 2023
And what output do you expect, just to clarify
andrewbranch commentedon May 30, 2023
https://github.com/andrewbranch/example-webpack-ts-extensions
I would be thrilled if it were a ts-loader bug, but I didn’t think loaders had a way of controlling this.
alexander-akait commentedon Jun 2, 2023
Thank you, I will look soon, webpack doesn't know when source code is stricky ESM, so I am afraid we can't do this without loader API,
mtsandcts, and eventsare unknown extensions 😄But I will provide more feedback later
alexander-akait commentedon Jun 4, 2023
@andrewbranch I looked at code and yeah,
type: "javascript/esm"solves the problem, technically it can be fixed here https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L633, just change it totest: /\.m(js|ts)$/i,, but:commonjs/other inmts? Is it always ESM? I don't know some small details so need to clarifyloaderContext._module.type = "javascript/esm";in ts-loader, here https://github.com/TypeStrong/ts-loader/blob/main/src/index.ts#L136 (it will not work forhappypack, but it is not popular usage, for happypack users the solution is settype: "javascript/esm"manually)I think the last solution is better because only ts-loader and typescript know when the source code is ESM (I think typescript can have API to get it, at the least there is
tsconfig.jsonto undestand it).Also I can implement public API to change
typeof a module (but it will require some dicussions some loaders may start to abuse it and this is not very good, although it is still possible) to avoid using_module.And yes - updating ts-loader docs for ESM will be great too.
Also my concern is that if we add
ctsandmts, other tools can start to ask do add them extensions too, this will make many settings inflexible and may create a situation when, with a new update of a tool we will need to change something, and this may be a breaking change(s).andrewbranch commentedon Jun 7, 2023
I agree that a loader would be a good place to set the module kind. It’s great that there’s a (private API?) way to do it already!
Only under unsupported combinations of tsconfig options. I’m looking into making those combinations illegal.
I think this would be great. Please keep me posted on these discussions. Thanks for looking into this!
alexander-akait commentedon Jun 7, 2023
Yeah, it will be good
Theoretically, we can fix it right now 😄 Also if we will use
loaderContext._module.type = "javascript/esm";we will support old webpack versions too, then we can implement public API to change it, so almost every webpack versions (for 5) will work with itandrewbranch commentedon Jun 7, 2023
Yeah. I’ll plan to open a PR on ts-loader (maybe some others?), but a public API would be good, just so I can be sure the functionality will be available in future Webpack versions.
8 remaining items
webpack-bot commentedon Jan 24, 2024
Issue was closed because of inactivity.
If you think this is still a valid issue, please file a new issue with additional information.
alexander-akait commentedon Jan 24, 2024
Still valid for ts-loader
andrewbranch commentedon Jan 24, 2024
Yes, a small update: I’m finally unblocked to work on the TS side of this issue and hope it will land in 5.5. Thanks for the patience!
alexander-akait commentedon Mar 21, 2024
@andrewbranch Friendly ping, any news? Do you need a help?
andrewbranch commentedon Mar 21, 2024
This is a prerequisite: microsoft/TypeScript#57896
If this lands within a week or two, I probably have time to do the second part in TS in time to ship in 5.5.
alexander-akait commentedon Dec 4, 2024
@andrewbranch Friendly ping
andrewbranch commentedon Dec 5, 2024
We don’t have plans to prioritize microsoft/TypeScript#54102 at this point—the impact seems fairly low. It would still be preferable if the module type in Webpack were the same between TS and JS files, but I haven’t heard any complaints of things going wrong because of it, so I can’t justify spending time to change things on ts-loader either. It’s fine with me if you want to close this.
alexander-akait commentedon Apr 16, 2025
@andrewbranch Just for information - there is an issue #17288 and it provides a good example why this issue is still make sense, briefly - when you set
javascript/esmwebpack makes more optimizations, under the hood we have theisStrictHarmonyproperty, it means module doesn't mixedrequire/import(and mixed commonjs/esm stuff) and so we can make more aggressive optimizationsandrewbranch commentedon Apr 17, 2025
This is #17288—did you mean to link a different one? 😄 Edit: I guess #19423
alexander-akait commentedon Apr 17, 2025
@andrewbranch Oh yes, sorry 😄