Skip to content

Commit cac0acb

Browse files
authored
Merge pull request #3321 from zelliott/path-mapping
[api-extractor] API Extractor now properly runs on projects with tsconfig path mappings.
2 parents a79a153 + e276078 commit cac0acb

File tree

11 files changed

+645
-91
lines changed

11 files changed

+645
-91
lines changed

apps/api-extractor/src/analyzer/AstSymbolTable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class AstSymbolTable {
118118
* Used to analyze an entry point that belongs to the working package.
119119
*/
120120
public fetchAstModuleFromWorkingPackage(sourceFile: ts.SourceFile): AstModule {
121-
return this._exportAnalyzer.fetchAstModuleFromSourceFile(sourceFile, undefined);
121+
return this._exportAnalyzer.fetchAstModuleFromSourceFile(sourceFile, undefined, false);
122122
}
123123

124124
/**

apps/api-extractor/src/analyzer/ExportAnalyzer.ts

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,6 @@ interface IAstModuleReference {
5656
* generating .d.ts rollups.
5757
*/
5858
export class ExportAnalyzer {
59-
// Captures "@a/b" or "d" from these examples:
60-
// @a/b
61-
// @a/b/c
62-
// d
63-
// d/
64-
// d/e
65-
private static _modulePathRegExp: RegExp = /^((?:@[^@\/\s]+\/)?[^@\/\s]+)(?:.*)$/;
66-
6759
private readonly _program: ts.Program;
6860
private readonly _typeChecker: ts.TypeChecker;
6961
private readonly _bundledPackageNames: ReadonlySet<string>;
@@ -94,10 +86,12 @@ export class ExportAnalyzer {
9486
*
9587
* @param moduleReference - contextual information about the import statement that took us to this source file.
9688
* or `undefined` if this source file is the initial entry point
89+
* @param isExternal - whether the given `moduleReference` is external.
9790
*/
9891
public fetchAstModuleFromSourceFile(
9992
sourceFile: ts.SourceFile,
100-
moduleReference: IAstModuleReference | undefined
93+
moduleReference: IAstModuleReference | undefined,
94+
isExternal: boolean
10195
): AstModule {
10296
const moduleSymbol: ts.Symbol = this._getModuleSymbolFromSourceFile(sourceFile, moduleReference);
10397

@@ -107,14 +101,8 @@ export class ExportAnalyzer {
107101
let astModule: AstModule | undefined = this._astModulesByModuleSymbol.get(moduleSymbol);
108102
if (!astModule) {
109103
// (If moduleReference === undefined, then this is the entry point of the local project being analyzed.)
110-
let externalModulePath: string | undefined = undefined;
111-
if (moduleReference !== undefined) {
112-
// Match: "@microsoft/sp-lodash-subset" or "lodash/has"
113-
// but ignore: "../folder/LocalFile"
114-
if (this._isExternalModulePath(moduleReference.moduleSpecifier)) {
115-
externalModulePath = moduleReference.moduleSpecifier;
116-
}
117-
}
104+
const externalModulePath: string | undefined =
105+
moduleReference !== undefined && isExternal ? moduleReference.moduleSpecifier : undefined;
118106

119107
astModule = new AstModule({ sourceFile, moduleSymbol, externalModulePath });
120108

@@ -266,29 +254,32 @@ export class ExportAnalyzer {
266254
/**
267255
* Returns true if the module specifier refers to an external package. Ignores packages listed in the
268256
* "bundledPackages" setting from the api-extractor.json config file.
269-
*
270-
* @remarks
271-
* Examples:
272-
*
273-
* - NO: `./file1`
274-
* - YES: `library1/path/path`
275-
* - YES: `@my-scope/my-package`
276257
*/
277-
private _isExternalModulePath(moduleSpecifier: string): boolean {
278-
if (ts.isExternalModuleNameRelative(moduleSpecifier)) {
258+
private _isExternalModulePath(
259+
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode,
260+
moduleSpecifier: string
261+
): boolean {
262+
const resolvedModule: ts.ResolvedModuleFull = this._getResolvedModule(
263+
importOrExportDeclaration,
264+
moduleSpecifier
265+
);
266+
267+
// Either something like `jquery` or `@microsoft/api-extractor`.
268+
const packageName: string | undefined = resolvedModule.packageId?.name;
269+
if (packageName !== undefined && this._bundledPackageNames.has(packageName)) {
279270
return false;
280271
}
281272

282-
const match: RegExpExecArray | null = ExportAnalyzer._modulePathRegExp.exec(moduleSpecifier);
283-
if (match) {
284-
// Extract "@my-scope/my-package" from "@my-scope/my-package/path/module"
285-
const packageName: string = match[1];
286-
if (this._bundledPackageNames.has(packageName)) {
287-
return false;
288-
}
273+
if (resolvedModule.isExternalLibraryImport === undefined) {
274+
// This presumably means the compiler couldn't figure out whether the module was external, but we're not
275+
// sure how this can happen.
276+
throw new InternalError(
277+
`Cannot determine whether the module ${JSON.stringify(moduleSpecifier)} is external\n` +
278+
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
279+
);
289280
}
290281

291-
return true;
282+
return resolvedModule.isExternalLibraryImport;
292283
}
293284

294285
/**
@@ -568,10 +559,7 @@ export class ExportAnalyzer {
568559

569560
// Ignore "export { A }" without a module specifier
570561
if (exportDeclaration.moduleSpecifier) {
571-
const externalModulePath: string | undefined = this._tryGetExternalModulePath(
572-
exportDeclaration,
573-
declarationSymbol
574-
);
562+
const externalModulePath: string | undefined = this._tryGetExternalModulePath(exportDeclaration);
575563

576564
if (externalModulePath !== undefined) {
577565
return this._fetchAstImport(declarationSymbol, {
@@ -597,10 +585,7 @@ export class ExportAnalyzer {
597585
TypeScriptHelpers.findFirstParent<ts.ImportDeclaration>(declaration, ts.SyntaxKind.ImportDeclaration);
598586

599587
if (importDeclaration) {
600-
const externalModulePath: string | undefined = this._tryGetExternalModulePath(
601-
importDeclaration,
602-
declarationSymbol
603-
);
588+
const externalModulePath: string | undefined = this._tryGetExternalModulePath(importDeclaration);
604589

605590
if (declaration.kind === ts.SyntaxKind.NamespaceImport) {
606591
// EXAMPLE:
@@ -852,22 +837,10 @@ export class ExportAnalyzer {
852837
}
853838

854839
private _tryGetExternalModulePath(
855-
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode,
856-
exportSymbol?: ts.Symbol
840+
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode
857841
): string | undefined {
858-
// The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point'
859-
const moduleSpecifier: string | undefined =
860-
TypeScriptHelpers.getModuleSpecifier(importOrExportDeclaration);
861-
if (!moduleSpecifier) {
862-
throw new InternalError(
863-
'Unable to parse module specifier\n' +
864-
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
865-
);
866-
}
867-
868-
// Match: "@microsoft/sp-lodash-subset" or "lodash/has"
869-
// but ignore: "../folder/LocalFile"
870-
if (this._isExternalModulePath(moduleSpecifier)) {
842+
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
843+
if (this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier)) {
871844
return moduleSpecifier;
872845
}
873846

@@ -882,32 +855,12 @@ export class ExportAnalyzer {
882855
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration,
883856
exportSymbol: ts.Symbol
884857
): AstModule {
885-
// The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point'
886-
const moduleSpecifier: string | undefined =
887-
TypeScriptHelpers.getModuleSpecifier(importOrExportDeclaration);
888-
if (!moduleSpecifier) {
889-
throw new InternalError(
890-
'Unable to parse module specifier\n' +
891-
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
892-
);
893-
}
894-
895-
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
896-
importOrExportDeclaration.getSourceFile(),
858+
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
859+
const resolvedModule: ts.ResolvedModuleFull = this._getResolvedModule(
860+
importOrExportDeclaration,
897861
moduleSpecifier
898862
);
899863

900-
if (resolvedModule === undefined) {
901-
// This should not happen, since getResolvedModule() specifically looks up names that the compiler
902-
// found in export declarations for this source file
903-
//
904-
// Encountered in https://github.com/microsoft/rushstack/issues/1914
905-
throw new InternalError(
906-
`getResolvedModule() could not resolve module name ${JSON.stringify(moduleSpecifier)}\n` +
907-
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
908-
);
909-
}
910-
911864
// Map the filename back to the corresponding SourceFile. This circuitous approach is needed because
912865
// we have no way to access the compiler's internal resolveExternalModuleName() function
913866
const moduleSourceFile: ts.SourceFile | undefined = this._program.getSourceFile(
@@ -922,13 +875,15 @@ export class ExportAnalyzer {
922875
);
923876
}
924877

878+
const isExternal: boolean = this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier);
925879
const moduleReference: IAstModuleReference = {
926880
moduleSpecifier: moduleSpecifier,
927881
moduleSpecifierSymbol: exportSymbol
928882
};
929883
const specifierAstModule: AstModule = this.fetchAstModuleFromSourceFile(
930884
moduleSourceFile,
931-
moduleReference
885+
moduleReference,
886+
isExternal
932887
);
933888

934889
return specifierAstModule;
@@ -963,4 +918,44 @@ export class ExportAnalyzer {
963918

964919
return astImport;
965920
}
921+
922+
private _getResolvedModule(
923+
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode,
924+
moduleSpecifier: string
925+
): ts.ResolvedModuleFull {
926+
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
927+
importOrExportDeclaration.getSourceFile(),
928+
moduleSpecifier
929+
);
930+
931+
if (resolvedModule === undefined) {
932+
// This should not happen, since getResolvedModule() specifically looks up names that the compiler
933+
// found in export declarations for this source file
934+
//
935+
// Encountered in https://github.com/microsoft/rushstack/issues/1914
936+
throw new InternalError(
937+
`getResolvedModule() could not resolve module name ${JSON.stringify(moduleSpecifier)}\n` +
938+
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
939+
);
940+
}
941+
942+
return resolvedModule;
943+
}
944+
945+
private _getModuleSpecifier(
946+
importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode
947+
): string {
948+
// The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point'
949+
const moduleSpecifier: string | undefined =
950+
TypeScriptHelpers.getModuleSpecifier(importOrExportDeclaration);
951+
952+
if (!moduleSpecifier) {
953+
throw new InternalError(
954+
'Unable to parse module specifier\n' +
955+
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration)
956+
);
957+
}
958+
959+
return moduleSpecifier;
960+
}
966961
}

build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ import { Lib1Class } from 'api-extractor-lib1-test';
88

99
export { Lib1Class }
1010

11-
1211
```

build-tests/api-extractor-scenarios/config/build-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"inconsistentReleaseTags",
3535
"internationalCharacters",
3636
"namedDefaultImport",
37+
"pathMappings",
3738
"preapproved",
3839
"spanSorting",
3940
"typeOf",

0 commit comments

Comments
 (0)