Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add experimental dts rollup using @microsoft/api-extractor #983

Merged
merged 15 commits into from Nov 17, 2023

Conversation

ocavue
Copy link
Sponsor Collaborator

@ocavue ocavue commented Aug 31, 2023

This PR introduces a new --experimental-dts flag to generate declaration files. This flag is intended to eventually replace the current --dts flag. As it's experimental, we can release it in a minor version without disrupting existing users. Once we have sufficient confidence in its functionality, we can deprecate the --dts flag and make --experimental-dts the default behavior in a major version.

Motivation

The --dts flag currently uses rollup-plugin-dts to generate declaration files. However, this plugin is no longer actively maintained1 and has known issues.

The --experimental-dts flag aims to improve upon this by using @microsoft/api-extractor to generate declaration files.

Limitations

One limitation of @microsoft/api-extractor is that it doesn't support multiple entry files. For instance, react-dom has multiple entry files,
including react-dom/client, react-dom/server, etc. Although there are plans2 to support multiple entry files, progress has been slow.
Therefore, I have implemented a workaround, which we'll discuss in the next section.

Implementation

Let's take an example, we have a library with the following structure:

my-lib/
├── src/
│   ├── client.ts
│   ├── server.ts
│   └── shared.ts
├── package.json
└── tsconfig.json

This library has two entry files, client.ts and server.ts. They both import shared.ts.

The first step is to generate declaration files using TypeScript compiler. The source code of this part is in src/tsc.ts. It will generate declaration files for the whole project, just like what tsc does. The generated declaration files will be put in a temporary directory .tsup. Here is the file structure after this step:

my-lib/
├── .tsup/
│   ├── .gitignore
│   └── declaration/
│       ├── client.d.ts
│       ├── server.d.ts
│       └── shared.d.ts
└── ...

We cannot run api-extractor separately for each entry file, because it will generate duplicated declaration for exported types in shared.ts. So we need to give it a single entry file. In src/tsc.ts, we use TypeScript to find all exported types for each entry file, and later we will generate a merged declaration file for them. Here is the file structure after this step:

my-lib/
├── .tsup/
│   ├── .gitignore
│   └── declaration/
│       ├── _tsup-dts-aggregation.d.ts
│       ├── client.d.ts
│       ├── server.d.ts
│       └── shared.d.ts
└── ...

The .gitignore file is used to ignore all files in .tsup directory, so users don't need to modify their .gitignore file. I tried to put it under node_modules/.tsup but it seems that api-extractor will ignore declaration files under node_modules.

In _tsup-dts-aggregation.d.ts, it will simply re-export all exported types from client.d.ts, server.d.ts. Here is the content of _tsup-dts-aggregation.d.ts:

export { render } from './client'
export { ClientRenderOptions } from './client'
export { render as render_alias_1 } from './server'
export { ServerRenderOptions } from './server'
export { default as default_alias } from './server'

Note that if there are multiple exported types with the same name, it will generate a unique name for them.

The next step will run api-extractor to generate declaration files. Api-extractor will read _tsup-dts-aggregation.d.ts and rollup all exported types into a single declaration file _tsup-dts-rollup.d.ts. Here is the file structure after this step:

my-lib/
├── dist/
│   └── _tsup-dts-rollup.d.ts
├── .tsup/
│   ├── .gitignore
│   └── declaration/
│       ├── _tsup-dts-aggregation.d.ts
│       ├── client.d.ts
│       ├── server.d.ts
│       └── shared.d.ts
└── ...

For the last step, we will create one declaration file for each entry file under the dist/, and re-export needed exported types from _tsup-dts-rollup.d.ts. Here is the final file structure and content:

my-lib/
├── dist/
│   ├── client.d.ts
│   ├── server.d.ts
│   └── _tsup-dts-rollup.d.ts
└── ...
// client.d.ts
export { render } from './_tsup-dts-rollup'
export { ClientRenderOptions } from './_tsup-dts-rollup'
// server.d.ts
export { render_alias_1 as render } from './_tsup-dts-rollup'
export { ServerRenderOptions } from './_tsup-dts-rollup'
export { default_alias as default } from './_tsup-dts-rollup'

Future Work

  • Conduct tests with more real-world libraries.
  • For the majority of libraries that only have one entry file, the --experimental-dts flag should use a shorter path to generate the declaration files (i.e., without the final _tsup-dts-rollup.d.ts file).
  • When using a star export to export all types from a third-party module, TypeScript will generate an export for each type in this module. For instance, export * from 'react-dom/server' will result in multiple specific exports like export { render } from 'react-dom/server' and export { renderToString } from 'react-dom/server', etc. This is not good because the exported types for a module could vary between different versions.
  • Ideally, api-extractor should handle multiple entry files. I am open to contributing to api-extractor to facilitate this.

Footnotes

  1. https://github.com/Swatinem/rollup-plugin-dts/issues/277

  2. https://github.com/microsoft/rushstack/issues/1596#issuecomment-546790721

@codesandbox
Copy link

codesandbox bot commented Aug 31, 2023

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel
Copy link

vercel bot commented Aug 31, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
tsup ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 17, 2023 0:25am

@socket-security
Copy link

socket-security bot commented Aug 31, 2023

New dependencies detected. Learn more about Socket for GitHub ↗︎

Packages Version New capabilities Transitives Size Publisher
@microsoft/api-extractor 7.38.3 eval, environment +2 3.53 MB odspnpm

@egoist
Copy link
Owner

egoist commented Sep 17, 2023

I think @microsoft/api-extractor can be a peer dependecy and use localRequire to load it on demand.

@bang9
Copy link

bang9 commented Oct 23, 2023

I am amazed to discover this.
We are using the api-extractor with the exact same logic to generate .d.ts files for our SDK build.

One issue I encountered was that, to ensure proper auto-import behavior in various IDEs, we needed to employ a little trick.
For example, to support auto-import of types existing in sub-entry .d.ts files in VSCode, we had to pre-import all the sub-entry .d.ts files from the main entry .d.ts file.

For reference: https://github.com/sendbird/sendbird-chat-sdk-javascript/blob/main/index.d.ts#L1-L5

@toteto
Copy link

toteto commented Nov 6, 2023

This is amazing work and looks promising! Any updates on the state of it?

@mechanical-turk
Copy link

This would be very cool. I'd love to use tsup for building declaration maps as well as everything else. "Go to definition" taking you to the source instead of the d.ts files would be a great devx improvement

@ocavue ocavue merged commit 61fc06c into egoist:dev Nov 17, 2023
9 checks passed
Copy link

🎉 This PR is included in version 8.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@timocov
Copy link

timocov commented Dec 29, 2023

by using @microsoft/api-extractor to generate declaration files.

@ocavue out of curiosity if you considered dts-bundle-generator as an alternative and if so, what was your opinion and pros/cons that leaned you towards api-extractor? Thanks!

@10Derozan
Copy link

by using @microsoft/api-extractor to generate declaration files.

@ocavue out of curiosity if you considered dts-bundle-generator as an alternative and if so, what was your opinion and pros/cons that leaned you towards api-extractor? Thanks!

I want to know it, too. Any updates? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants