rushstack
rushstack copied to clipboard
[api-extractor] Inconsistent rollups behaviour between moduleResolution
Summary
In my project, I have libraries that generate types using vite-plugin-dts which uses underneath api-extractor.
When I migrated the tsconfig.json from moduleResolution: node to moduleResolution: bundler I realised that the output .d.ts was not identical.
Repro steps
The following repository is configured to use the api-extractor on the same code, but with 3 different moduleResolution: node, node16 and bundler.`
git clone [email protected]:Lukinoh/repro-api-extractor-bug(Stackblitz)cd repro-api-extractor-bugnpm installnpm run build- Look at the
distfolders, it contains for eachmoduleResolution:- The result of the
tscbuild - The extracted
.d.ts - The diagnostics
- The result of the
The extracted .d.ts of node16 and bundler are different from node.
I would have expected to have the content.
Expected result:
declare class ClassExample {
field1: number;
field2: string;
}
export declare const functions: ClassExample[];
export { }
Actual result:
import { ClassExample } from './hop/f1.ts';
export declare const functions: ClassExample[];
export { }
Standard questions
Please answer these questions to help us investigate your issue more quickly:
| Question | Answer |
|---|---|
@microsoft/api-extractor version? |
7.49.2 |
| Operating system? | Linux (WSL2) |
| API Extractor scenario? | rollups (.d.ts) |
| Would you consider contributing a PR? | No |
| TypeScript compiler version? | 5.7.2 |
Node.js version (node -v)? |
22.14.0 |
I did some debugging, and I think I found the root cause (at least within the api-extractor app) of this issue. The reason the output appears this way is because the imported type's module is treated as external, which I believe is a bug in ExportAnalyzer._isExternalModulePath. Here's the main call flow that's occuring:
ExportAnalyzer._isExternalModulePath- calls
TypeScriptInternals.getResolvedModule- calls
ts.program.getResolvedModule- returns
undefined
- returns
- returns
undefined
- calls
- cannot resolve to module, so returns
true
- calls
Why does ts.program.getResolvedModule return undefined?
- In the resolvedModules underlying cache, the imported module is there but has a mode of 99 (
ts.ModuleKind.ESNext)- Note: the cache only uses a mode with a moduleResolution of "bundler" or "node16", but it doesn't with "node", which is why we see a discrepancy
- Note: the mode is also still
ts.ModuleKind.ESNext, even if yourtsconfig.jsonspecifies a different target/module version (as far as I can tell)
- However, we supply a mode of undefined, so it's a miss and the module is unable to be resolved, thus it is treated as an external module
Why is mode undefined?
- The mode is retrieved by calling
TypeScriptInternals.getModeForUsageLocation, but only if the specifier is a string literal. - This is determined by the
ts.isStringLiteralLikefunction, which just matchests.SyntaxKind.StringLiteralorts.SyntaxKind.NoSubstitutionTemplateLiteral. - However, the specifier is a
ts.SyntaxKind.Literal, so it doesn't pass the check. - This regression was likely caused by updating to TypeScript 5.7.2.
Unfortunately, what should be done here is now out of my depth. I'm not sure why the import specifier is not a string literal, and I'm not sure how to work around that. @octogonz could you weigh in on this?