developit / microbundle

📦 Zero-configuration bundler for tiny modules.
https://npm.im/microbundle
MIT License
8k stars 361 forks source link

Split TS declaration files are not compatible with nodenext module resolution #1019

Closed ssangervasi closed 1 year ago

ssangervasi commented 1 year ago

TLDR: Microbundle should output a single .d.ts per output module file, or nodenext is too unstable to target at all.

Background

This issue is a funky mixture of a few other issues:

  1. The ongoing changes to TS's module resolution, where nodenext (aka node16) uses modules: https://github.com/microsoft/TypeScript/issues/50152
  2. Microbundle's basic documentation of nodenext compat: https://github.com/developit/microbundle/issues/990
  3. And the fact microbundle generates multiple .d.ts files for a single .*js output: https://github.com/developit/microbundle/issues/239

Reproducing

I forked the code from #239 and updated it to highlight the problem with nodenext: https://github.com/ssangervasi/microbundle-typings. In this example, you can see:

  1. :heavy_check_mark: The "library" package is built with microbundle
  2. :heavy_check_mark: The library package has exports.types pointing to the module's types, dist/index.d.ts, as suggested by microbundle's readme.
  3. :neutral_face: That declaration file has a relative dependency on another file produced by microbundle.
  4. :heavy_checkmark: The dependent package is built with tsc 4.9.4 with nodenext. (This is the thing I want the users of my package to be able to do.)_
  5. :heavy_check_mark: The dependent package can resolve the library package's root declaration.
  6. :disappointed: TypeScript can't resolve the relative dependency produced by microbundle.

Proposed solution

My instinct is that generating multiple .d.ts files for a single output module is going to cause problems in general. In this case, if the build process could merge them (the same way it merges the JS into a single file), the TS resolution problem would go away. Basically reopening https://github.com/developit/microbundle/issues/239.

On the other hand, TS's moduleResolution is madenning*, and it's possible this relative lookup will go away in later versions. Microbundle's readme could be updated to note this incompatibility for now. As far as I can tell, there isn't a way to get these split .d.ts files to work, except maybe package.json redirects which I'm not willing to try or publish.

* On par with how complex JS's dependency system is.

rschristian commented 1 year ago

There's an easy fix:

// src/index.ts
-export * from "./foo";
+export * from "./foo.js";

Longer answer: This really isn't a Microbundle issue, but a TS issue. You'll run into this same behavior using tsc directly. See https://github.com/microsoft/TypeScript/issues/16577, https://github.com/microsoft/TypeScript/issues/49083

The TS team have drawn a line in the sand and refuse to rewrite module specifiers as there's no TS features there; instead, you're supposed to use .js to refer to .ts and .tsx files. There have been dozens of issues opened up on the TS repo for this, but it's the way they've decided to go. We're (more or less) using tsc here, so our behavior matches the official compiler.

While merging declaration files could be nice, it's not without it's own complexities and it would be a move away from tsc which increases the maintenance burden. Outputting multiple .d.ts files for a single output is default tsc behavior, it's not a choice we made or anything.

ssangervasi commented 1 year ago

Holy moly, I read about that but still couldn't understand the fix applied to this situation.

TypeScript will impose Node’s much stricter ESM resolution algorithm on those files, disabling index-file resolution and extensionless lookups—in fact, the extension the user has to write is .js, which will be nonsensical for the context, where the runtime module resolver (the bundler) only ever sees .ts files.

Thanks for explaining!

rschristian commented 1 year ago

No problem, it's certainly a weird one. Using foo.js to refer to foo.d.ts file is quite the odd design choice to most users I think.