microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.2k stars 12.38k forks source link

`moduleResolution: bundler` diverges from bundlers following node semantics in regards to `default` import #54102

Open Andarist opened 1 year ago

Andarist commented 1 year ago

Bug Report

šŸ”Ž Search Terms

moduleResolution bundler node webpack default namespace

šŸ•— Version & Regression Information

āÆ Playground Link

Repro case can be found here

šŸ™ Actual behavior

When resolving a .d.mts file that proxies to .d.ts in a CJS package with moduleResolution: bundler, TypeScript complains with an error like:

Property 'default' does not exist on type 'string'.ts(2339)

The reason that it complains about it is that TypeScript is resolving default import as if the .d.ts in this CJS package could export the "real" default export. webpack actually follows semantics coined by node and it loads the namespace object as the default export of that "cjs file".

So the type-level reality diverges there from runtime. At runtime, with webpack - it works exactly like it would work in node. TypeScript thinks that the intention was to load module.exports.default there though.

šŸ™‚ Expected behavior

I'm not exactly sure what's the expected behavior here because "resolving" like a bundler is quite under-specified and bundlers are not quite consistent when it comes to this. Even when we check this very same repro, we can see that I had to use defaultIsModuleExports: true in Rollup for it to behave the same. By default, Rollup behaves in the same way as TypeScript does.

cc @andrewbranch

fatcerberus commented 1 year ago

Isnā€™t this controlled by allowSyntheticDefaultImports?

andrewbranch commented 1 year ago

Yeah, Iā€™m aware of this, but of course itā€™s not a moduleResolution issue. Really, itā€™s something that module should handle. I believe behavior differs between different bundlers, which is, in technical terms, a huge bummer. Whatā€™s really bad, IMO, is that last I checked, Webpack does the Node behavior for .{m,c}js files, but not the equivalent TS extensions, which violates a pretty core assumption we have about the substitutability of declaration files, TS files, and JS files. This is already on my radar to investigate soon.

Andarist commented 1 year ago

If you ever need a hug... I'm here for you.

andrewbranch commented 1 year ago

I forked https://sokra.github.io/interop-test to https://andrewbranch.github.io/interop-test, removed a bunch of import and export variations that werenā€™t super interesting to me (there are still a ton), and added .ts/.mts cases to esbuild and Webpack. I need to add similar TS cases to Rollup, and Iā€™d like to add Parcel, Bun, and perhaps others. But it shows basically what weā€™ve already established in this issue:

At first, it struck me as odd that these bundlers voluntarily adopted interop restrictions that Node tried very hard to avoid, but couldnā€™t for technical reasons. But doing so is the best way for them to handle code written for Nodeā€”itā€™s a cross-compatibility strategy. I think it would be best to push all bundlers toward doing this, if theyā€™re not already. But since TypeScript isnā€™t yet set up to model this behavior (without also adopting nodenext module resolution), it seems inevitable that if we do, it will be toggleable with compiler options somehow. So even if we canā€™t get bundlers to be consistent between each other on whether they apply this Node-compat behavior, users will be able to configure TypeScript for whatever their bundler does.

However, itā€™s not possible for us to model bundlers like Webpack that apply the Node-compat behavior to .mjs files, but not to .mts files. Thatā€™s something weā€™ll need to push for across the bundler/TS-runtime ecosystem (Iā€™m assuming Webpack isnā€™t the only one that will need changes) before any TS option can work.

Andarist commented 1 year ago

However, itā€™s not possible for us to model bundlers like Webpack that apply the Node-compat behavior to .mjs files, but not to .mts files. Thatā€™s something weā€™ll need to push for across the bundler/TS-runtime ecosystem (Iā€™m assuming Webpack isnā€™t the only one that will need changes) before any TS option can work.

I recall that you mentioned knowing about this issue for quite some time. Is there some tracking issue about this in webpack? Did their team comment on this anyhow in the past?

andrewbranch commented 1 year ago

I donā€™t know; Iā€™m in the process of figuring out exactly what to propose, and plan to talk to Sean soon. I havenā€™t mentioned it earlier because it was something I noticed in passing while focusing on module resolution, and needed to double check that I wasnā€™t just holding it wrong šŸ„“

andrewbranch commented 1 year ago

I opened https://github.com/webpack/webpack/issues/17288, and also am showing relevant results at https://andrewbranch.github.io/interop-test/#synthesizing-default-exports-for-cjs-modules

andrewbranch commented 3 months ago

@Andarist do you remember how you originally encountered this issue, i.e. what libraries were involved?

Andarist commented 3 months ago

@andrewbranch likely it comes from me and @emmatown trying to implement our library building scheme that is meant to avoid dual package hazard and to provide consistent module shape across formats. We ended up emitting an extra proxy files to work around it. Notice the _default vs default dance introduced here: https://github.com/preconstruct/preconstruct/pull/546