From 1f6f97db0dc7c66f712ed31742e5264deb9b4882 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 10:40:35 +0800 Subject: [PATCH 01/11] add import() type test case --- .../api-extractor-scenarios.api.json | 141 +++++++++++++++++- .../api-extractor-scenarios.api.md | 15 +- .../etc/dynamicImportType/rollup.d.ts | 23 ++- .../api-extractor-scenarios.api.json | 92 ++++++++++++ .../api-extractor-scenarios.api.md | 14 +- .../etc/dynamicImportType2/rollup.d.ts | 10 +- .../src/dynamicImportType/Item.ts | 6 +- .../src/dynamicImportType/Options.ts | 2 + .../src/dynamicImportType/re-export.ts | 2 + .../src/dynamicImportType2/index.ts | 4 + .../src/dynamicImportType2/local-module.ts | 3 + .../dynamicImportType2/namespace-export.ts | 7 + 12 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 build-tests/api-extractor-scenarios/src/dynamicImportType2/local-module.ts create mode 100644 build-tests/api-extractor-scenarios/src/dynamicImportType2/namespace-export.ts diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json index 3a7783606e3..689d44df26b 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json @@ -188,6 +188,36 @@ "name": "Item", "preserveMemberOrder": false, "members": [ + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#externalModule:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "externalModule: " + }, + { + "kind": "Content", + "text": "typeof import('api-extractor-lib3-test')" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "externalModule", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "api-extractor-scenarios!Item#lib1:member", @@ -330,12 +360,47 @@ }, { "kind": "Property", - "canonicalReference": "api-extractor-scenarios!Item#reExport:member", + "canonicalReference": "api-extractor-scenarios!Item#reExportExternal:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "reExportExternal: " + }, + { + "kind": "Content", + "text": "import('./re-export')." + }, + { + "kind": "Reference", + "text": "Lib3Class", + "canonicalReference": "api-extractor-lib3-test!Lib3Class:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "reExportExternal", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#reExportLocal:member", "docComment": "", "excerptTokens": [ { "kind": "Content", - "text": "reExport: " + "text": "reExportLocal: " }, { "kind": "Content", @@ -354,7 +419,77 @@ "isReadonly": false, "isOptional": false, "releaseTag": "Public", - "name": "reExport", + "name": "reExportLocal", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#typeofImportExternal:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "typeofImportExternal: " + }, + { + "kind": "Content", + "text": "typeof import('api-extractor-lib3-test')." + }, + { + "kind": "Reference", + "text": "Lib1Class", + "canonicalReference": "api-extractor-lib1-test!Lib1Class:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "typeofImportExternal", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#typeofImportLocal:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "typeofImportLocal: " + }, + { + "kind": "Content", + "text": "typeof import('./Options')." + }, + { + "kind": "Reference", + "text": "OptionsClass", + "canonicalReference": "api-extractor-scenarios!~OptionsClass:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "typeofImportLocal", "propertyTypeTokenRange": { "startIndex": 1, "endIndex": 3 diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md index 999494f5feb..09abdd8e615 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md @@ -4,6 +4,7 @@ ```ts +import { apiExtractorLib3Test } from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; import { Lib1Class } from 'api-extractor-lib3-test'; import { Lib1Interface } from 'api-extractor-lib1-test'; @@ -12,6 +13,8 @@ import { Lib2Interface } from 'api-extractor-lib2-test'; // @public (undocumented) export class Item { + // (undocumented) + externalModule: apiExtractorLib3Test; // (undocumented) lib1: Lib1Interface; // (undocumented) @@ -22,8 +25,18 @@ export class Item { // // (undocumented) options: Options; + // Warning: (ae-forgotten-export) The symbol "Lib3Class" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reExportExternal: Lib3Class; + // (undocumented) + reExportLocal: Lib2Class; + // (undocumented) + typeofImportExternal: Lib1Class; + // Warning: (ae-forgotten-export) The symbol "OptionsClass" needs to be exported by the entry point index.d.ts + // // (undocumented) - reExport: Lib2Class; + typeofImportLocal: OptionsClass; } export { Lib1 } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts index 93bc8c69f24..f3bba961810 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts @@ -1,3 +1,4 @@ +import { apiExtractorLib3Test } from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; import { Lib1Class } from 'api-extractor-lib3-test'; import { Lib1Interface } from 'api-extractor-lib1-test'; @@ -10,16 +11,36 @@ export declare class Item { lib1: Lib1Interface; lib2: Lib2Interface; lib3: Lib1Class; - reExport: Lib2Class; + externalModule: apiExtractorLib3Test; + typeofImportLocal: OptionsClass; + typeofImportExternal: Lib1Class; + reExportLocal: Lib2Class; + reExportExternal: Lib3Class; } export { Lib1 } export { Lib2Interface } +/** + * @internalRemarks Internal remarks + * @public + */ +declare class Lib3Class { + /** + * I am a documented property! + * @betaDocumentation My docs include a custom block tag! + * @virtual @override + */ + prop: boolean; +} + declare interface Options { name: string; color: 'red' | 'blue'; } +declare class OptionsClass { +} + export { } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json index b7a2b150998..50f21d5438c 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json @@ -258,6 +258,98 @@ "startIndex": 1, "endIndex": 4 } + }, + { + "kind": "PropertySignature", + "canonicalReference": "api-extractor-scenarios!IExample#localDottedImportType:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "localDottedImportType: " + }, + { + "kind": "Content", + "text": "import('./namespace-export')." + }, + { + "kind": "Reference", + "text": "LocalModule.LocalClass", + "canonicalReference": "api-extractor-scenarios!~LocalClass:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "localDottedImportType", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "api-extractor-scenarios!IExample#localDottedImportType2:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "localDottedImportType2: " + }, + { + "kind": "Content", + "text": "import('./namespace-export')." + }, + { + "kind": "Reference", + "text": "LocalNS.LocalNSClass", + "canonicalReference": "api-extractor-scenarios!~LocalNS.LocalNSClass:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "localDottedImportType2", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, + { + "kind": "PropertySignature", + "canonicalReference": "api-extractor-scenarios!IExample#predefinedNamedImport:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "predefinedNamedImport: " + }, + { + "kind": "Reference", + "text": "Lib1Namespace.Inner.X", + "canonicalReference": "api-extractor-lib1-test!Lib1Namespace.Inner.X:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "predefinedNamedImport", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } } ], "extendsTokenRanges": [] diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md index 46ecc077ba9..937072cf206 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md @@ -9,9 +9,19 @@ import { Lib1Namespace } from 'api-extractor-lib1-test'; // @public (undocumented) export interface IExample { // (undocumented) - dottedImportType: Lib1Namespace.Inner.X | undefined; + dottedImportType: Lib1Namespace | undefined; // (undocumented) - dottedImportType2: Lib1Namespace.Y | undefined; + dottedImportType2: Lib1Namespace | undefined; + // Warning: (ae-forgotten-export) The symbol "LocalClass" needs to be exported by the entry point index.d.ts + // + // (undocumented) + localDottedImportType: LocalClass; + // Warning: (ae-forgotten-export) The symbol "LocalNS" needs to be exported by the entry point index.d.ts + // + // (undocumented) + localDottedImportType2: import('./namespace-export').LocalNS.LocalNSClass; + // (undocumented) + predefinedNamedImport: Lib1Namespace.Inner.X; } // (No @packageDocumentation comment for this package) diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts index c8d465c2ca5..c68efa22301 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts @@ -2,8 +2,14 @@ import { Lib1Namespace } from 'api-extractor-lib1-test'; /** @public */ export declare interface IExample { - dottedImportType: Lib1Namespace.Inner.X | undefined; - dottedImportType2: Lib1Namespace.Y | undefined; + predefinedNamedImport: Lib1Namespace.Inner.X; + dottedImportType: Lib1Namespace | undefined; + dottedImportType2: Lib1Namespace | undefined; + localDottedImportType: LocalClass; + localDottedImportType2: import('./namespace-export').LocalNS.LocalNSClass; +} + +declare class LocalClass { } export { } diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts index 5f638c0854c..949d103a09f 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts @@ -7,5 +7,9 @@ export class Item { lib1: import('api-extractor-lib1-test').Lib1Interface; lib2: import('api-extractor-lib2-test').Lib2Interface; lib3: import('api-extractor-lib3-test').Lib1Class; - reExport: import('./re-export').Lib2Class; + externalModule: typeof import('api-extractor-lib3-test'); + typeofImportLocal: typeof import('./Options').OptionsClass; + typeofImportExternal: typeof import('api-extractor-lib3-test').Lib1Class; + reExportLocal: import('./re-export').Lib2Class; + reExportExternal: import('./re-export').Lib3Class; } diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType/Options.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType/Options.ts index 938f0a920d0..9280b77276d 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType/Options.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType/Options.ts @@ -5,3 +5,5 @@ export interface Options { name: string; color: 'red' | 'blue'; } + +export class OptionsClass {} diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType/re-export.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType/re-export.ts index 6f1dedc68cf..decfe0cd89f 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType/re-export.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType/re-export.ts @@ -1,4 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +export * from 'api-extractor-lib3-test'; + export { Lib2Class } from 'api-extractor-lib2-test'; diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType2/index.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType2/index.ts index 5d211e8470e..eee2fae57a1 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType2/index.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType2/index.ts @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { Lib1Namespace } from 'api-extractor-lib1-test'; /** @public */ export interface IExample { + predefinedNamedImport: Lib1Namespace.Inner.X; dottedImportType: import('api-extractor-lib1-test').Lib1Namespace.Inner.X | undefined; dottedImportType2: import('api-extractor-lib1-test').Lib1Namespace.Y | undefined; + localDottedImportType: import('./namespace-export').LocalModule.LocalClass; + localDottedImportType2: import('./namespace-export').LocalNS.LocalNSClass; } diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType2/local-module.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType2/local-module.ts new file mode 100644 index 00000000000..4d6133b75ae --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType2/local-module.ts @@ -0,0 +1,3 @@ +export class LocalClass {} + +export interface LocalInterface {} diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType2/namespace-export.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType2/namespace-export.ts new file mode 100644 index 00000000000..4508abf2461 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType2/namespace-export.ts @@ -0,0 +1,7 @@ +import * as LocalModule from './local-module'; +export { LocalModule }; + +export namespace LocalNS { + export class LocalNSClass {} + export interface LocalNSInterface {} +} From 93627ce437aa8a5e818c059128cff432b16c7e45 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 10:02:56 +0800 Subject: [PATCH 02/11] refactor import() type support --- .../src/analyzer/AstSubPathImport.ts | 29 +++ .../src/analyzer/AstSymbolTable.ts | 31 +-- .../src/analyzer/ExportAnalyzer.ts | 180 ++++++++++-------- apps/api-extractor/src/collector/Collector.ts | 7 + .../src/generators/DtsEmitHelpers.ts | 23 ++- 5 files changed, 174 insertions(+), 96 deletions(-) create mode 100644 apps/api-extractor/src/analyzer/AstSubPathImport.ts diff --git a/apps/api-extractor/src/analyzer/AstSubPathImport.ts b/apps/api-extractor/src/analyzer/AstSubPathImport.ts new file mode 100644 index 00000000000..679bba4b3c8 --- /dev/null +++ b/apps/api-extractor/src/analyzer/AstSubPathImport.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { AstEntity, AstSyntheticEntity } from './AstEntity'; + +export interface IAstSubPathImportOptions { + readonly astEntity: AstEntity; + readonly exportPath: string[]; +} + +export class AstSubPathImport extends AstSyntheticEntity { + public readonly astEntity: AstEntity; + + public readonly exportPath: string[]; + + public isImportTypeEverywhere: boolean = true; + + public constructor(options: IAstSubPathImportOptions) { + super(); + this.astEntity = options.astEntity; + this.exportPath = options.exportPath; + } + + /** {@inheritdoc} */ + public get localName(): string { + // abstract + return this.exportPath[this.exportPath.length - 1]; + } +} diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index e589ea10097..a4c0bf4f892 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -18,6 +18,7 @@ import type { MessageRouter } from '../collector/MessageRouter'; import { TypeScriptInternals, type IGlobalVariableAnalyzer } from './TypeScriptInternals'; import { SyntaxHelpers } from './SyntaxHelpers'; import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; +import { AstSubPathImport } from './AstSubPathImport'; /** * Options for `AstSymbolTable._fetchAstSymbol()` @@ -318,21 +319,29 @@ export class AstSymbolTable { // If this symbol is non-external (i.e. it belongs to the working package), then we also analyze any // referencedAstSymbols that are non-external. For example, this ensures that forgotten exports // get analyzed. - rootAstSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { - for (const referencedAstEntity of astDeclaration.referencedAstEntities) { - // Walk up to the root of the tree, looking for any imports along the way - if (referencedAstEntity instanceof AstSymbol) { - if (!referencedAstEntity.isExternal) { - this._analyzeAstSymbol(referencedAstEntity); - } + const analyzeReferencedLocalAstEntity = (referencedAstEntity: AstEntity) => { + if (referencedAstEntity instanceof AstSymbol) { + if (!referencedAstEntity.isExternal) { + this._analyzeAstSymbol(referencedAstEntity); } + } - if (referencedAstEntity instanceof AstNamespaceImport) { - if (!referencedAstEntity.astModule.isExternal) { - this._analyzeAstNamespaceImport(referencedAstEntity); - } + if (referencedAstEntity instanceof AstNamespaceImport) { + if (!referencedAstEntity.astModule.isExternal) { + this._analyzeAstNamespaceImport(referencedAstEntity); } } + + if (referencedAstEntity instanceof AstSubPathImport) { + analyzeReferencedLocalAstEntity(referencedAstEntity.astEntity); + } + }; + + rootAstSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + for (const referencedAstEntity of astDeclaration.referencedAstEntities) { + // Walk up to the root of the tree, looking for any imports along the way + analyzeReferencedLocalAstEntity(referencedAstEntity); + } }); } } diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index d8a68a2b76c..df313b98f98 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -15,6 +15,7 @@ import type { AstEntity } from './AstEntity'; import { AstNamespaceImport } from './AstNamespaceImport'; import { SyntaxHelpers } from './SyntaxHelpers'; import { AstNamespaceExport } from './AstNamespaceExport'; +import { AstSubPathImport } from './AstSubPathImport'; /** * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. @@ -68,6 +69,10 @@ export class ExportAnalyzer { private readonly _importableAmbientSourceFiles: Set = new Set(); private readonly _astImportsByKey: Map = new Map(); + private readonly _astSubPathImportsByKey: Map> = new Map< + AstEntity, + Map + >(); private readonly _astNamespaceImportByModule: Map = new Map(); public constructor( @@ -431,6 +436,17 @@ export class ExportAnalyzer { return astSymbol; } + private _collectIdentifierPath(node: ts.EntityName): ts.Identifier[] { + const identifiers: ts.Identifier[] = []; + let leftNode: ts.EntityName = node; + while (leftNode.kind === ts.SyntaxKind.QualifiedName) { + identifiers.unshift(leftNode.right); + leftNode = leftNode.left; + } + identifiers.unshift(leftNode); + return identifiers; + } + public fetchReferencedAstEntityFromImportTypeNode( node: ts.ImportTypeNode, referringModuleIsExternal: boolean @@ -438,90 +454,70 @@ export class ExportAnalyzer { const externalModulePath: string | undefined = this._tryGetExternalModulePath(node); if (externalModulePath) { - let exportName: string; if (node.qualifier) { // Example input: // import('api-extractor-lib1-test').Lib1GenericType // - // Extracted qualifier: - // Lib1GenericType - exportName = node.qualifier.getText().trim(); + // Extracted import base: + // import { Lib1GenericType } from 'api-extractor-lib1-test'; + const fullExportPath: ts.Identifier[] = this._collectIdentifierPath(node.qualifier); + const exportName: string = fullExportPath[0].getText().trim(); + // There is no symbol property in a ImportTypeNode, obtain the associated export symbol + const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(fullExportPath[0]); + let exportAstEntity: AstEntity; + if (exportName === ts.InternalSymbolName.Default) { + exportAstEntity = this._fetchAstImport(exportSymbol, { + importKind: AstImportKind.DefaultImport, + modulePath: externalModulePath, + exportName: exportSymbol?.name ?? exportName, + isTypeOnly: true + }); + } else { + exportAstEntity = this._fetchAstImport(exportSymbol, { + importKind: AstImportKind.NamedImport, + exportName: exportName, + modulePath: externalModulePath, + isTypeOnly: true + }); + } + return this._fetchAstSubPathImport( + exportAstEntity, + fullExportPath.slice(1).map((id) => id.getText().trim()) + ); } else { // Example input: // import('api-extractor-lib1-test') // - // Extracted qualifier: - // apiExtractorLib1Test + // Extracted import base: + // import * as apiExtractorLib1Test from 'api-extractor-lib1-test'; - exportName = SyntaxHelpers.makeCamelCaseIdentifier(externalModulePath); + // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for + // a package or source file. + return this._fetchAstImport(undefined, { + importKind: AstImportKind.StarImport, + exportName: SyntaxHelpers.makeCamelCaseIdentifier(externalModulePath), + modulePath: externalModulePath, + isTypeOnly: false + }); } - - return this._fetchAstImport(undefined, { - importKind: AstImportKind.ImportType, - exportName: exportName, - modulePath: externalModulePath, - isTypeOnly: false - }); } - // Internal reference: AstSymbol - const rightMostToken: ts.Identifier | ts.ImportTypeNode = node.qualifier - ? node.qualifier.kind === ts.SyntaxKind.QualifiedName - ? node.qualifier.right - : node.qualifier - : node; - - // There is no symbol property in a ImportTypeNode, obtain the associated export symbol - const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(rightMostToken); - if (!exportSymbol) { + // Internal reference + if (node.qualifier) { + const fullExportPath: ts.Identifier[] = this._collectIdentifierPath(node.qualifier); + const exportName: ts.Identifier = fullExportPath[0]; + const astModule: AstModule = this._fetchSpecifierAstModule(node, undefined); + const exportAstEntity: AstEntity = this._getExportOfAstModule(exportName.getText().trim(), astModule); + return this._fetchAstSubPathImport( + exportAstEntity, + fullExportPath.slice(1).map((id) => id.getText().trim()) + ); + } else { throw new InternalError( - `Symbol not found for identifier: ${node.getText()}\n` + + `import() for local module without qualifier is not supported: ${node.getText()}\n` + SourceFileLocationFormatter.formatDeclaration(node) ); } - - let followedSymbol: ts.Symbol = exportSymbol; - for (;;) { - const referencedAstEntity: AstEntity | undefined = this.fetchReferencedAstEntity( - followedSymbol, - referringModuleIsExternal - ); - - if (referencedAstEntity) { - return referencedAstEntity; - } - - const followedSymbolNode: ts.Node | ts.ImportTypeNode | undefined = - followedSymbol.declarations && (followedSymbol.declarations[0] as ts.Node | undefined); - - if (followedSymbolNode && followedSymbolNode.kind === ts.SyntaxKind.ImportType) { - return this.fetchReferencedAstEntityFromImportTypeNode( - followedSymbolNode as ts.ImportTypeNode, - referringModuleIsExternal - ); - } - - // eslint-disable-next-line no-bitwise - if (!(followedSymbol.flags & ts.SymbolFlags.Alias)) { - break; - } - - const currentAlias: ts.Symbol = this._typeChecker.getAliasedSymbol(followedSymbol); - if (!currentAlias || currentAlias === followedSymbol) { - break; - } - - followedSymbol = currentAlias; - } - - const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({ - followedSymbol: followedSymbol, - isExternal: referringModuleIsExternal, - includeNominalAnalysis: false, - addIfMissing: true - }); - - return astSymbol; } private _tryMatchExportDeclaration( @@ -901,16 +897,20 @@ export class ExportAnalyzer { * and fetches the corresponding AstModule object. */ private _fetchSpecifierAstModule( - importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration, - exportSymbol: ts.Symbol + importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode, + exportSymbol: ts.Symbol | undefined ): AstModule { const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration); + const moduleSpecifierNode: ts.Expression | undefined = ts.isImportTypeNode(importOrExportDeclaration) + ? ts.isLiteralTypeNode(importOrExportDeclaration.argument) + ? importOrExportDeclaration.argument.literal + : undefined + : importOrExportDeclaration.moduleSpecifier; const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined = - importOrExportDeclaration.moduleSpecifier && - ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier) + moduleSpecifierNode && ts.isStringLiteralLike(moduleSpecifierNode) ? TypeScriptInternals.getModeForUsageLocation( importOrExportDeclaration.getSourceFile(), - importOrExportDeclaration.moduleSpecifier, + moduleSpecifierNode, this._program.getCompilerOptions() ) : undefined; @@ -948,10 +948,12 @@ export class ExportAnalyzer { } const isExternal: boolean = this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier); - const moduleReference: IAstModuleReference = { - moduleSpecifier: moduleSpecifier, - moduleSpecifierSymbol: exportSymbol - }; + const moduleReference: IAstModuleReference | undefined = exportSymbol + ? { + moduleSpecifier: moduleSpecifier, + moduleSpecifierSymbol: exportSymbol + } + : undefined; const specifierAstModule: AstModule = this.fetchAstModuleFromSourceFile( moduleSourceFile, moduleReference, @@ -991,6 +993,30 @@ export class ExportAnalyzer { return astImport; } + private _fetchAstSubPathImport(astEntity: AstEntity, exportPath: string[]): AstEntity { + if (exportPath.length === 0) { + return astEntity; + } + + let astSubPathImportsByExportPath: Map | undefined = + this._astSubPathImportsByKey.get(astEntity); + if (astSubPathImportsByExportPath === undefined) { + astSubPathImportsByExportPath = new Map(); + this._astSubPathImportsByKey.set(astEntity, astSubPathImportsByExportPath); + } + + const exportPathKey: string = exportPath.join('.'); + let astSubPathImport: AstSubPathImport | undefined = astSubPathImportsByExportPath.get(exportPathKey); + if (astSubPathImport === undefined) { + astSubPathImport = new AstSubPathImport({ + astEntity: astEntity, + exportPath: exportPath + }); + astSubPathImportsByExportPath.set(exportPathKey, astSubPathImport); + } + return astSubPathImport; + } + private _getModuleSpecifier( importOrExportDeclaration: ts.ImportDeclaration | ts.ExportDeclaration | ts.ImportTypeNode ): string { diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 8f6d74320ed..f89ee0fd367 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -34,6 +34,7 @@ import { ExtractorConfig } from '../api/ExtractorConfig'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; import { AstImport } from '../analyzer/AstImport'; import type { SourceMapper } from './SourceMapper'; +import { AstSubPathImport } from '../analyzer/AstSubPathImport'; /** * Options for Collector constructor. @@ -578,6 +579,12 @@ export class Collector { this._recursivelyCreateEntities(localAstEntity, alreadySeenAstEntities); } } + + if (astEntity instanceof AstSubPathImport) { + this._createCollectorEntity(astEntity.astEntity); + this._createCollectorEntity(astEntity); + this._recursivelyCreateEntities(astEntity.astEntity, alreadySeenAstEntities); + } } /** diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 3e7a339115e..140891c2f2f 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -11,6 +11,7 @@ import type { Collector } from '../collector/Collector'; import type { Span } from '../analyzer/Span'; import type { IndentedWriter } from './IndentedWriter'; import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; +import { AstSubPathImport } from '../analyzer/AstSubPathImport'; /** * Some common code shared between DtsRollupGenerator and ApiReportGenerator. @@ -109,6 +110,8 @@ export class DtsEmitHelpers { throw new InternalError('referencedEntry.nameForEmit is undefined'); } + const typeofPrefix: string = node.isTypeOf ? 'typeof ' : ''; + let typeArgumentsText: string = ''; if (node.typeArguments && node.typeArguments.length > 0) { @@ -146,11 +149,7 @@ export class DtsEmitHelpers { const separatorAfter: string = /(\s*)$/.exec(span.getText())?.[1] ?? ''; - if ( - referencedEntity.astEntity instanceof AstImport && - referencedEntity.astEntity.importKind === AstImportKind.ImportType && - referencedEntity.astEntity.exportName - ) { + if (referencedEntity.astEntity instanceof AstSubPathImport) { // For an ImportType with a namespace chain, only the top namespace is imported. // Must add the original nested qualifiers to the rolled up import. const qualifiersText: string = node.qualifier?.getText() ?? ''; @@ -158,15 +157,23 @@ export class DtsEmitHelpers { // Including the leading "." const nestedQualifiersText: string = nestedQualifiersStart >= 0 ? qualifiersText.substring(nestedQualifiersStart) : ''; - - const replacement: string = `${referencedEntity.nameForEmit}${nestedQualifiersText}${typeArgumentsText}${separatorAfter}`; + const baseEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity( + referencedEntity.astEntity.astEntity + ); + if (!baseEntity) { + throw new InternalError( + `Collector entity for import() not found: ${node.getText()}\n` + + SourceFileLocationFormatter.formatDeclaration(node) + ); + } + const replacement: string = `${typeofPrefix}${baseEntity.nameForEmit}${nestedQualifiersText}${typeArgumentsText}${separatorAfter}`; span.modification.skipAll(); span.modification.prefix = replacement; } else { // Replace with internal symbol or AstImport span.modification.skipAll(); - span.modification.prefix = `${referencedEntity.nameForEmit}${typeArgumentsText}${separatorAfter}`; + span.modification.prefix = `${typeofPrefix}${referencedEntity.nameForEmit}${typeArgumentsText}${separatorAfter}`; } } } From 088ddd9c58bf850e63c879bb24e784694e3f1886 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 10:50:14 +0800 Subject: [PATCH 03/11] re-run build --- .../api-extractor-scenarios.api.md | 15 +++++------ .../etc/dynamicImportType/rollup.d.ts | 26 +++++------------- .../api-extractor-scenarios.api.json | 2 +- .../api-extractor-scenarios.api.md | 12 +++------ .../etc/dynamicImportType2/rollup.d.ts | 27 +++++++++++++++---- .../api-extractor-scenarios.api.md | 4 +-- .../etc/dynamicImportType3/rollup.d.ts | 4 +-- 7 files changed, 45 insertions(+), 45 deletions(-) diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md index 09abdd8e615..edb285b7e3a 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md @@ -4,17 +4,18 @@ ```ts -import { apiExtractorLib3Test } from 'api-extractor-lib3-test'; +import * as apiExtractorLib3Test from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; -import { Lib1Class } from 'api-extractor-lib3-test'; -import { Lib1Interface } from 'api-extractor-lib1-test'; +import type { Lib1Class } from 'api-extractor-lib3-test'; +import type { Lib1Interface } from 'api-extractor-lib1-test'; import { Lib2Class } from 'api-extractor-lib2-test'; import { Lib2Interface } from 'api-extractor-lib2-test'; +import { Lib3Class } from 'api-extractor-lib3-test'; // @public (undocumented) export class Item { // (undocumented) - externalModule: apiExtractorLib3Test; + externalModule: typeof apiExtractorLib3Test; // (undocumented) lib1: Lib1Interface; // (undocumented) @@ -25,18 +26,16 @@ export class Item { // // (undocumented) options: Options; - // Warning: (ae-forgotten-export) The symbol "Lib3Class" needs to be exported by the entry point index.d.ts - // // (undocumented) reExportExternal: Lib3Class; // (undocumented) reExportLocal: Lib2Class; // (undocumented) - typeofImportExternal: Lib1Class; + typeofImportExternal: typeof Lib1Class; // Warning: (ae-forgotten-export) The symbol "OptionsClass" needs to be exported by the entry point index.d.ts // // (undocumented) - typeofImportLocal: OptionsClass; + typeofImportLocal: typeof OptionsClass; } export { Lib1 } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts index f3bba961810..ea5bc0c9fa7 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts @@ -1,9 +1,10 @@ -import { apiExtractorLib3Test } from 'api-extractor-lib3-test'; +import * as apiExtractorLib3Test from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; -import { Lib1Class } from 'api-extractor-lib3-test'; -import { Lib1Interface } from 'api-extractor-lib1-test'; +import type { Lib1Class } from 'api-extractor-lib3-test'; +import type { Lib1Interface } from 'api-extractor-lib1-test'; import { Lib2Class } from 'api-extractor-lib2-test'; import { Lib2Interface } from 'api-extractor-lib2-test'; +import { Lib3Class } from 'api-extractor-lib3-test'; /** @public */ export declare class Item { @@ -11,9 +12,9 @@ export declare class Item { lib1: Lib1Interface; lib2: Lib2Interface; lib3: Lib1Class; - externalModule: apiExtractorLib3Test; - typeofImportLocal: OptionsClass; - typeofImportExternal: Lib1Class; + externalModule: typeof apiExtractorLib3Test; + typeofImportLocal: typeof OptionsClass; + typeofImportExternal: typeof Lib1Class; reExportLocal: Lib2Class; reExportExternal: Lib3Class; } @@ -22,19 +23,6 @@ export { Lib1 } export { Lib2Interface } -/** - * @internalRemarks Internal remarks - * @public - */ -declare class Lib3Class { - /** - * I am a documented property! - * @betaDocumentation My docs include a custom block tag! - * @virtual @override - */ - prop: boolean; -} - declare interface Options { name: string; color: 'red' | 'blue'; diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json index 50f21d5438c..393c60766e6 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json @@ -275,7 +275,7 @@ { "kind": "Reference", "text": "LocalModule.LocalClass", - "canonicalReference": "api-extractor-scenarios!~LocalClass:class" + "canonicalReference": "api-extractor-scenarios!~LocalClass_2:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md index 937072cf206..5c30ced621c 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.md @@ -9,17 +9,13 @@ import { Lib1Namespace } from 'api-extractor-lib1-test'; // @public (undocumented) export interface IExample { // (undocumented) - dottedImportType: Lib1Namespace | undefined; + dottedImportType: Lib1Namespace.Inner.X | undefined; // (undocumented) - dottedImportType2: Lib1Namespace | undefined; - // Warning: (ae-forgotten-export) The symbol "LocalClass" needs to be exported by the entry point index.d.ts - // + dottedImportType2: Lib1Namespace.Y | undefined; // (undocumented) - localDottedImportType: LocalClass; - // Warning: (ae-forgotten-export) The symbol "LocalNS" needs to be exported by the entry point index.d.ts - // + localDottedImportType: LocalModule.LocalClass; // (undocumented) - localDottedImportType2: import('./namespace-export').LocalNS.LocalNSClass; + localDottedImportType2: LocalNS.LocalNSClass; // (undocumented) predefinedNamedImport: Lib1Namespace.Inner.X; } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts index c68efa22301..08fabbba796 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/rollup.d.ts @@ -3,13 +3,30 @@ import { Lib1Namespace } from 'api-extractor-lib1-test'; /** @public */ export declare interface IExample { predefinedNamedImport: Lib1Namespace.Inner.X; - dottedImportType: Lib1Namespace | undefined; - dottedImportType2: Lib1Namespace | undefined; - localDottedImportType: LocalClass; - localDottedImportType2: import('./namespace-export').LocalNS.LocalNSClass; + dottedImportType: Lib1Namespace.Inner.X | undefined; + dottedImportType2: Lib1Namespace.Y | undefined; + localDottedImportType: LocalModule.LocalClass; + localDottedImportType2: LocalNS.LocalNSClass; } -declare class LocalClass { +declare class LocalClass_2 { +} + +declare interface LocalInterface { +} + +declare namespace LocalModule { + export { + LocalClass_2 as LocalClass, + LocalInterface + } +} + +declare namespace LocalNS { + class LocalNSClass { + } + interface LocalNSInterface { + } } export { } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType3/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType3/api-extractor-scenarios.api.md index ee8c43fff7c..c663e207a88 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType3/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType3/api-extractor-scenarios.api.md @@ -4,8 +4,8 @@ ```ts -import { Lib1GenericType } from 'api-extractor-lib1-test'; -import { Lib1Interface } from 'api-extractor-lib1-test'; +import type { Lib1GenericType } from 'api-extractor-lib1-test'; +import type { Lib1Interface } from 'api-extractor-lib1-test'; // @public (undocumented) export interface IExample { diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType3/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType3/rollup.d.ts index dcbb364c050..06d40f3f74e 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType3/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType3/rollup.d.ts @@ -1,5 +1,5 @@ -import { Lib1GenericType } from 'api-extractor-lib1-test'; -import { Lib1Interface } from 'api-extractor-lib1-test'; +import type { Lib1GenericType } from 'api-extractor-lib1-test'; +import type { Lib1Interface } from 'api-extractor-lib1-test'; /** @public */ export declare interface IExample { From 57bacf9bebb34d4aa394a96b23a1fbe2b3e4dfe2 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 13:54:41 +0800 Subject: [PATCH 04/11] clean code: remove AstImport of ImportType; update comments; modifyImportTypeSpan by exportPath --- apps/api-extractor/src/analyzer/AstImport.ts | 15 +------ .../src/analyzer/AstSubPathImport.ts | 41 ++++++++++++++++++- .../src/analyzer/AstSymbolTable.ts | 2 +- .../src/analyzer/ExportAnalyzer.ts | 2 + apps/api-extractor/src/collector/Collector.ts | 4 +- .../src/generators/DtsEmitHelpers.ts | 27 ++---------- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts index bc2b685949d..5d17a19b48f 100644 --- a/apps/api-extractor/src/analyzer/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -27,12 +27,7 @@ export enum AstImportKind { /** * An import statement such as `import x = require("y");`. */ - EqualsImport, - - /** - * An import statement such as `interface foo { foo: import("bar").a.b.c }`. - */ - ImportType + EqualsImport } /** @@ -150,14 +145,6 @@ export class AstImport extends AstSyntheticEntity { return `${options.modulePath}:*`; case AstImportKind.EqualsImport: return `${options.modulePath}:=`; - case AstImportKind.ImportType: { - const subKey: string = !options.exportName - ? '*' // Equivalent to StarImport - : options.exportName.includes('.') // Equivalent to a named export - ? options.exportName.split('.')[0] - : options.exportName; - return `${options.modulePath}:${subKey}`; - } default: throw new InternalError('Unknown AstImportKind'); } diff --git a/apps/api-extractor/src/analyzer/AstSubPathImport.ts b/apps/api-extractor/src/analyzer/AstSubPathImport.ts index 679bba4b3c8..9ee22037bc0 100644 --- a/apps/api-extractor/src/analyzer/AstSubPathImport.ts +++ b/apps/api-extractor/src/analyzer/AstSubPathImport.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { InternalError } from '@rushstack/node-core-library'; import { AstEntity, AstSyntheticEntity } from './AstEntity'; export interface IAstSubPathImportOptions { @@ -8,16 +9,52 @@ export interface IAstSubPathImportOptions { readonly exportPath: string[]; } +/** + * `AstSubPathImport` represents a sub-path import, such as `import("foo").X.Y.Z` will import Y.Z from X of "foo". + * + * @remarks + * + * The base AstEntity can be either local or external entity. + * + * ```ts + * // import from AstImport (NamedImport, modulePath="foo", exportName="X"), with exportPath ["Y", "Z"] + * const foo: import("foo").X.Y.Z; + * + * // import from AstImport (DefaultImport, modulePath="foo"), with exportPath ["X", "Y", "Z"] + * const bar: import("foo").default.X.Y.Z; + * + * // import from AstEntity of X, with exportPath ["Y", "Z"] + * const baz: import("./foo").X.Y.Z; + * ``` + */ export class AstSubPathImport extends AstSyntheticEntity { - public readonly astEntity: AstEntity; + /** + * The AstEntity that is imported from. + * + * @remarks + * + * The AstEntity can be either local or external entity. + */ + public readonly baseAstEntity: AstEntity; + /** + * The path to the entity within the `baseAstEntity`. + */ public readonly exportPath: string[]; + /** + * Whether it is referenced only by import type syntax, e.g. `import("foo").Bar`. + */ public isImportTypeEverywhere: boolean = true; public constructor(options: IAstSubPathImportOptions) { super(); - this.astEntity = options.astEntity; + + if (options.exportPath.length === 0) { + throw new InternalError('AstSubPathImport.exportPath cannot be empty'); + } + + this.baseAstEntity = options.astEntity; this.exportPath = options.exportPath; } diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index a4c0bf4f892..1093101f7fe 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -333,7 +333,7 @@ export class AstSymbolTable { } if (referencedAstEntity instanceof AstSubPathImport) { - analyzeReferencedLocalAstEntity(referencedAstEntity.astEntity); + analyzeReferencedLocalAstEntity(referencedAstEntity.baseAstEntity); } }; diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index df313b98f98..8d278d2ca48 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -995,6 +995,8 @@ export class ExportAnalyzer { private _fetchAstSubPathImport(astEntity: AstEntity, exportPath: string[]): AstEntity { if (exportPath.length === 0) { + // If the exportPath is empty, just use the AstEntity directly instead of creating an unnecessary AstSubPathImport. + // e.g. return AstImport for `import("foo").Bar`, return AstEntity of Bar for `import("./foo").Bar`. return astEntity; } diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index f89ee0fd367..a87cba0e609 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -581,9 +581,9 @@ export class Collector { } if (astEntity instanceof AstSubPathImport) { - this._createCollectorEntity(astEntity.astEntity); + this._createCollectorEntity(astEntity.baseAstEntity); this._createCollectorEntity(astEntity); - this._recursivelyCreateEntities(astEntity.astEntity, alreadySeenAstEntities); + this._recursivelyCreateEntities(astEntity.baseAstEntity, alreadySeenAstEntities); } } diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 140891c2f2f..7c61434ac40 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -51,21 +51,6 @@ export class DtsEmitHelpers { `${importPrefix} ${collectorEntity.nameForEmit} = require('${astImport.modulePath}');` ); break; - case AstImportKind.ImportType: - if (!astImport.exportName) { - writer.writeLine( - `${importPrefix} * as ${collectorEntity.nameForEmit} from '${astImport.modulePath}';` - ); - } else { - const topExportName: string = astImport.exportName.split('.')[0]; - if (collectorEntity.nameForEmit === topExportName) { - writer.write(`${importPrefix} { ${topExportName} }`); - } else { - writer.write(`${importPrefix} { ${topExportName} as ${collectorEntity.nameForEmit} }`); - } - writer.writeLine(` from '${astImport.modulePath}';`); - } - break; default: throw new InternalError('Unimplemented AstImportKind'); } @@ -150,15 +135,8 @@ export class DtsEmitHelpers { const separatorAfter: string = /(\s*)$/.exec(span.getText())?.[1] ?? ''; if (referencedEntity.astEntity instanceof AstSubPathImport) { - // For an ImportType with a namespace chain, only the top namespace is imported. - // Must add the original nested qualifiers to the rolled up import. - const qualifiersText: string = node.qualifier?.getText() ?? ''; - const nestedQualifiersStart: number = qualifiersText.indexOf('.'); - // Including the leading "." - const nestedQualifiersText: string = - nestedQualifiersStart >= 0 ? qualifiersText.substring(nestedQualifiersStart) : ''; const baseEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity( - referencedEntity.astEntity.astEntity + referencedEntity.astEntity.baseAstEntity ); if (!baseEntity) { throw new InternalError( @@ -166,7 +144,8 @@ export class DtsEmitHelpers { SourceFileLocationFormatter.formatDeclaration(node) ); } - const replacement: string = `${typeofPrefix}${baseEntity.nameForEmit}${nestedQualifiersText}${typeArgumentsText}${separatorAfter}`; + const exportPathText: string = referencedEntity.astEntity.exportPath.join('.'); + const replacement: string = `${typeofPrefix}${baseEntity.nameForEmit}.${exportPathText}${typeArgumentsText}${separatorAfter}`; span.modification.skipAll(); span.modification.prefix = replacement; From 1e9ef275301acbe575205dc040161e5176885c64 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 13:56:54 +0800 Subject: [PATCH 05/11] rush change --- ...eat-api-extractor-import-type_2025-08-28-05-55.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft/api-extractor/feat-api-extractor-import-type_2025-08-28-05-55.json diff --git a/common/changes/@microsoft/api-extractor/feat-api-extractor-import-type_2025-08-28-05-55.json b/common/changes/@microsoft/api-extractor/feat-api-extractor-import-type_2025-08-28-05-55.json new file mode 100644 index 00000000000..c665ab293a9 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/feat-api-extractor-import-type_2025-08-28-05-55.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "refactor import() type support, to support more cases", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file From 8953f18a5c09631d900f9cced68c8dcfb653ffd9 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 14:07:11 +0800 Subject: [PATCH 06/11] remove isImportTypeEverywhere --- apps/api-extractor/src/analyzer/AstSubPathImport.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSubPathImport.ts b/apps/api-extractor/src/analyzer/AstSubPathImport.ts index 9ee22037bc0..aa7ad8b5806 100644 --- a/apps/api-extractor/src/analyzer/AstSubPathImport.ts +++ b/apps/api-extractor/src/analyzer/AstSubPathImport.ts @@ -42,11 +42,6 @@ export class AstSubPathImport extends AstSyntheticEntity { */ public readonly exportPath: string[]; - /** - * Whether it is referenced only by import type syntax, e.g. `import("foo").Bar`. - */ - public isImportTypeEverywhere: boolean = true; - public constructor(options: IAstSubPathImportOptions) { super(); From db95ad72433c07325d5466ba3ebe84aadd87f3af Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 14:40:25 +0800 Subject: [PATCH 07/11] update default import --- .../src/analyzer/ExportAnalyzer.ts | 2 +- .../api-extractor-scenarios.api.json | 35 +++++++++++++++++++ .../api-extractor-scenarios.api.md | 3 ++ .../etc/dynamicImportType/rollup.d.ts | 2 ++ .../api-extractor-scenarios.api.md | 3 +- .../etc/namedDefaultImport/rollup.d.ts | 3 +- .../src/dynamicImportType/Item.ts | 1 + 7 files changed, 46 insertions(+), 3 deletions(-) diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 8d278d2ca48..1dcefbd09d1 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -469,7 +469,7 @@ export class ExportAnalyzer { exportAstEntity = this._fetchAstImport(exportSymbol, { importKind: AstImportKind.DefaultImport, modulePath: externalModulePath, - exportName: exportSymbol?.name ?? exportName, + exportName: SyntaxHelpers.makeCamelCaseIdentifier(externalModulePath), isTypeOnly: true }); } else { diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json index 689d44df26b..83b1d9ef718 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json @@ -188,6 +188,41 @@ "name": "Item", "preserveMemberOrder": false, "members": [ + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#defaultImport:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "defaultImport: " + }, + { + "kind": "Content", + "text": "import('api-extractor-lib2-test')." + }, + { + "kind": "Reference", + "text": "default", + "canonicalReference": "api-extractor-lib2-test!DefaultClass:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "defaultImport", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "api-extractor-scenarios!Item#externalModule:member", diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md index edb285b7e3a..b2b94eebb56 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md @@ -4,6 +4,7 @@ ```ts +import type apiExtractorLib2Test from 'api-extractor-lib2-test'; import * as apiExtractorLib3Test from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; import type { Lib1Class } from 'api-extractor-lib3-test'; @@ -14,6 +15,8 @@ import { Lib3Class } from 'api-extractor-lib3-test'; // @public (undocumented) export class Item { + // (undocumented) + defaultImport: apiExtractorLib2Test; // (undocumented) externalModule: typeof apiExtractorLib3Test; // (undocumented) diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts index ea5bc0c9fa7..64b34b505bc 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts @@ -1,3 +1,4 @@ +import type apiExtractorLib2Test from 'api-extractor-lib2-test'; import * as apiExtractorLib3Test from 'api-extractor-lib3-test'; import * as Lib1 from 'api-extractor-lib1-test'; import type { Lib1Class } from 'api-extractor-lib3-test'; @@ -12,6 +13,7 @@ export declare class Item { lib1: Lib1Interface; lib2: Lib2Interface; lib3: Lib1Class; + defaultImport: apiExtractorLib2Test; externalModule: typeof apiExtractorLib3Test; typeofImportLocal: typeof OptionsClass; typeofImportExternal: typeof Lib1Class; diff --git a/build-tests/api-extractor-scenarios/etc/namedDefaultImport/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/namedDefaultImport/api-extractor-scenarios.api.md index 286bbca3991..a47e6542cb2 100644 --- a/build-tests/api-extractor-scenarios/etc/namedDefaultImport/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/namedDefaultImport/api-extractor-scenarios.api.md @@ -4,12 +4,13 @@ ```ts +import type apiExtractorLib2Test from 'api-extractor-lib2-test'; import { default as default_2 } from 'api-extractor-lib2-test'; // @public (undocumented) export interface DefaultImportTypes { // (undocumented) - dynamicImport: default_2; + dynamicImport: apiExtractorLib2Test; // (undocumented) namedImport: default_2; // (undocumented) diff --git a/build-tests/api-extractor-scenarios/etc/namedDefaultImport/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/namedDefaultImport/rollup.d.ts index 0bf69c07b82..4563c831830 100644 --- a/build-tests/api-extractor-scenarios/etc/namedDefaultImport/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/namedDefaultImport/rollup.d.ts @@ -1,10 +1,11 @@ +import type apiExtractorLib2Test from 'api-extractor-lib2-test'; import { default as default_2 } from 'api-extractor-lib2-test'; /** @public */ export declare interface DefaultImportTypes { namedImport: default_2; reExport: default_2; - dynamicImport: default_2; + dynamicImport: apiExtractorLib2Test; } export { } diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts index 949d103a09f..df8be3dc223 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts @@ -7,6 +7,7 @@ export class Item { lib1: import('api-extractor-lib1-test').Lib1Interface; lib2: import('api-extractor-lib2-test').Lib2Interface; lib3: import('api-extractor-lib3-test').Lib1Class; + defaultImport: import('api-extractor-lib2-test').default; externalModule: typeof import('api-extractor-lib3-test'); typeofImportLocal: typeof import('./Options').OptionsClass; typeofImportExternal: typeof import('api-extractor-lib3-test').Lib1Class; From 02532b23a57e71e6e70b8e08c0dc2de5dfcae0f2 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 14:55:05 +0800 Subject: [PATCH 08/11] clean code --- .../src/analyzer/ExportAnalyzer.ts | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 1dcefbd09d1..a1a024ea718 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -436,36 +436,34 @@ export class ExportAnalyzer { return astSymbol; } - private _collectIdentifierPath(node: ts.EntityName): ts.Identifier[] { - const identifiers: ts.Identifier[] = []; - let leftNode: ts.EntityName = node; - while (leftNode.kind === ts.SyntaxKind.QualifiedName) { - identifiers.unshift(leftNode.right); - leftNode = leftNode.left; - } - identifiers.unshift(leftNode); - return identifiers; - } - public fetchReferencedAstEntityFromImportTypeNode( node: ts.ImportTypeNode, referringModuleIsExternal: boolean ): AstEntity | undefined { + const importPath: ts.Identifier[] = []; + if (node.qualifier) { + let leftNode: ts.EntityName = node.qualifier; + while (leftNode.kind === ts.SyntaxKind.QualifiedName) { + importPath.unshift(leftNode.right); + leftNode = leftNode.left; + } + importPath.unshift(leftNode); + } + const externalModulePath: string | undefined = this._tryGetExternalModulePath(node); if (externalModulePath) { - if (node.qualifier) { - // Example input: - // import('api-extractor-lib1-test').Lib1GenericType - // - // Extracted import base: - // import { Lib1GenericType } from 'api-extractor-lib1-test'; - const fullExportPath: ts.Identifier[] = this._collectIdentifierPath(node.qualifier); - const exportName: string = fullExportPath[0].getText().trim(); + if (importPath.length > 0) { + const exportName: string = importPath[0].getText().trim(); // There is no symbol property in a ImportTypeNode, obtain the associated export symbol - const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(fullExportPath[0]); + const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(importPath[0]); let exportAstEntity: AstEntity; if (exportName === ts.InternalSymbolName.Default) { + // Example input: + // import('api-extractor-lib1-test').default + // + // Extracted import base: + // import apiExtractorLib1Test from 'api-extractor-lib1-test'; exportAstEntity = this._fetchAstImport(exportSymbol, { importKind: AstImportKind.DefaultImport, modulePath: externalModulePath, @@ -473,6 +471,11 @@ export class ExportAnalyzer { isTypeOnly: true }); } else { + // Example input: + // import('api-extractor-lib1-test').Lib1GenericType + // + // Extracted import base: + // import { Lib1GenericType } from 'api-extractor-lib1-test'; exportAstEntity = this._fetchAstImport(exportSymbol, { importKind: AstImportKind.NamedImport, exportName: exportName, @@ -482,7 +485,7 @@ export class ExportAnalyzer { } return this._fetchAstSubPathImport( exportAstEntity, - fullExportPath.slice(1).map((id) => id.getText().trim()) + importPath.slice(1).map((id) => id.getText().trim()) ); } else { // Example input: @@ -503,14 +506,13 @@ export class ExportAnalyzer { } // Internal reference - if (node.qualifier) { - const fullExportPath: ts.Identifier[] = this._collectIdentifierPath(node.qualifier); - const exportName: ts.Identifier = fullExportPath[0]; + if (importPath.length > 0) { + const exportName: ts.Identifier = importPath[0]; const astModule: AstModule = this._fetchSpecifierAstModule(node, undefined); const exportAstEntity: AstEntity = this._getExportOfAstModule(exportName.getText().trim(), astModule); return this._fetchAstSubPathImport( exportAstEntity, - fullExportPath.slice(1).map((id) => id.getText().trim()) + importPath.slice(1).map((id) => id.getText().trim()) ); } else { throw new InternalError( From 167018875ba9523b52324d33fc1baee4b7f50252 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Thu, 28 Aug 2025 16:37:44 +0800 Subject: [PATCH 09/11] clean code --- .../src/analyzer/AstSubPathImport.ts | 4 +-- .../src/analyzer/ExportAnalyzer.ts | 35 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSubPathImport.ts b/apps/api-extractor/src/analyzer/AstSubPathImport.ts index aa7ad8b5806..bf7a622674e 100644 --- a/apps/api-extractor/src/analyzer/AstSubPathImport.ts +++ b/apps/api-extractor/src/analyzer/AstSubPathImport.ts @@ -18,13 +18,13 @@ export interface IAstSubPathImportOptions { * * ```ts * // import from AstImport (NamedImport, modulePath="foo", exportName="X"), with exportPath ["Y", "Z"] - * const foo: import("foo").X.Y.Z; + * const bar: import("foo").X.Y.Z; * * // import from AstImport (DefaultImport, modulePath="foo"), with exportPath ["X", "Y", "Z"] * const bar: import("foo").default.X.Y.Z; * * // import from AstEntity of X, with exportPath ["Y", "Z"] - * const baz: import("./foo").X.Y.Z; + * const bar: import("./foo").X.Y.Z; * ``` */ export class AstSubPathImport extends AstSyntheticEntity { diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index a1a024ea718..60d21d7a626 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -15,7 +15,7 @@ import type { AstEntity } from './AstEntity'; import { AstNamespaceImport } from './AstNamespaceImport'; import { SyntaxHelpers } from './SyntaxHelpers'; import { AstNamespaceExport } from './AstNamespaceExport'; -import { AstSubPathImport } from './AstSubPathImport'; +import { AstSubPathImport, IAstSubPathImportOptions } from './AstSubPathImport'; /** * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. @@ -483,10 +483,10 @@ export class ExportAnalyzer { isTypeOnly: true }); } - return this._fetchAstSubPathImport( - exportAstEntity, - importPath.slice(1).map((id) => id.getText().trim()) - ); + return this._fetchAstSubPathImport({ + astEntity: exportAstEntity, + exportPath: importPath.slice(1).map((id) => id.getText().trim()) + }); } else { // Example input: // import('api-extractor-lib1-test') @@ -510,10 +510,10 @@ export class ExportAnalyzer { const exportName: ts.Identifier = importPath[0]; const astModule: AstModule = this._fetchSpecifierAstModule(node, undefined); const exportAstEntity: AstEntity = this._getExportOfAstModule(exportName.getText().trim(), astModule); - return this._fetchAstSubPathImport( - exportAstEntity, - importPath.slice(1).map((id) => id.getText().trim()) - ); + return this._fetchAstSubPathImport({ + astEntity: exportAstEntity, + exportPath: importPath.slice(1).map((id) => id.getText().trim()) + }); } else { throw new InternalError( `import() for local module without qualifier is not supported: ${node.getText()}\n` + @@ -995,27 +995,24 @@ export class ExportAnalyzer { return astImport; } - private _fetchAstSubPathImport(astEntity: AstEntity, exportPath: string[]): AstEntity { - if (exportPath.length === 0) { + private _fetchAstSubPathImport(options: IAstSubPathImportOptions): AstEntity { + if (options.exportPath.length === 0) { // If the exportPath is empty, just use the AstEntity directly instead of creating an unnecessary AstSubPathImport. // e.g. return AstImport for `import("foo").Bar`, return AstEntity of Bar for `import("./foo").Bar`. - return astEntity; + return options.astEntity; } let astSubPathImportsByExportPath: Map | undefined = - this._astSubPathImportsByKey.get(astEntity); + this._astSubPathImportsByKey.get(options.astEntity); if (astSubPathImportsByExportPath === undefined) { astSubPathImportsByExportPath = new Map(); - this._astSubPathImportsByKey.set(astEntity, astSubPathImportsByExportPath); + this._astSubPathImportsByKey.set(options.astEntity, astSubPathImportsByExportPath); } - const exportPathKey: string = exportPath.join('.'); + const exportPathKey: string = options.exportPath.join('.'); let astSubPathImport: AstSubPathImport | undefined = astSubPathImportsByExportPath.get(exportPathKey); if (astSubPathImport === undefined) { - astSubPathImport = new AstSubPathImport({ - astEntity: astEntity, - exportPath: exportPath - }); + astSubPathImport = new AstSubPathImport(options); astSubPathImportsByExportPath.set(exportPathKey, astSubPathImport); } return astSubPathImport; From ae35602e690b0b4285f577040cf06c48ec699eef Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Mon, 1 Sep 2025 19:33:36 +0800 Subject: [PATCH 10/11] support local AstNamespaceImport --- .../src/analyzer/AstNamespaceImport.ts | 9 +---- .../src/analyzer/ExportAnalyzer.ts | 22 +++++------- apps/api-extractor/src/collector/Collector.ts | 2 +- .../DeclarationReferenceGenerator.ts | 19 ++++------- .../api-extractor-scenarios.api.json | 34 +++++++++++++++++-- .../api-extractor-scenarios.api.md | 4 +++ .../etc/dynamicImportType/rollup.d.ts | 8 +++++ .../api-extractor-scenarios.api.json | 2 +- .../api-extractor-scenarios.api.json | 2 +- .../api-extractor-scenarios.api.json | 2 +- .../api-extractor-scenarios.api.json | 2 +- .../src/dynamicImportType/Item.ts | 1 + 12 files changed, 67 insertions(+), 40 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstNamespaceImport.ts b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts index 09304b9efcb..baa8bf707f2 100644 --- a/apps/api-extractor/src/analyzer/AstNamespaceImport.ts +++ b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts @@ -11,7 +11,6 @@ export interface IAstNamespaceImportOptions { readonly astModule: AstModule; readonly namespaceName: string; readonly declaration: ts.Declaration; - readonly symbol: ts.Symbol; } /** @@ -64,21 +63,15 @@ export class AstNamespaceImport extends AstSyntheticEntity { public readonly namespaceName: string; /** - * The original `ts.SyntaxKind.NamespaceImport` which can be used as a location for error messages. + * The (first) original `ts.SyntaxKind.NamespaceImport` (or `ts.SyntaxKind.SourceFile` if import declaration doesn't exist) which can be used as a location for error messages. */ public readonly declaration: ts.Declaration; - /** - * The original `ts.SymbolFlags.Namespace` symbol. - */ - public readonly symbol: ts.Symbol; - public constructor(options: IAstNamespaceImportOptions) { super(); this.astModule = options.astModule; this.namespaceName = options.namespaceName; this.declaration = options.declaration; - this.symbol = options.symbol; } /** {@inheritdoc} */ diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 60d21d7a626..501ff1f225f 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -506,19 +506,17 @@ export class ExportAnalyzer { } // Internal reference + const astModule: AstModule = this._fetchSpecifierAstModule(node, undefined); if (importPath.length > 0) { const exportName: ts.Identifier = importPath[0]; - const astModule: AstModule = this._fetchSpecifierAstModule(node, undefined); const exportAstEntity: AstEntity = this._getExportOfAstModule(exportName.getText().trim(), astModule); return this._fetchAstSubPathImport({ astEntity: exportAstEntity, exportPath: importPath.slice(1).map((id) => id.getText().trim()) }); } else { - throw new InternalError( - `import() for local module without qualifier is not supported: ${node.getText()}\n` + - SourceFileLocationFormatter.formatDeclaration(node) - ); + const localName: string = SyntaxHelpers.makeCamelCaseIdentifier(this._getModuleSpecifier(node)); + return this._getAstNamespaceImport(astModule, localName, astModule.sourceFile); } } @@ -603,15 +601,14 @@ export class ExportAnalyzer { ): AstNamespaceExport { const imoprtNamespace: AstNamespaceImport = this._getAstNamespaceImport( astModule, - declarationSymbol, + declarationSymbol.name, declaration ); return new AstNamespaceExport({ namespaceName: imoprtNamespace.localName, astModule: astModule, - declaration, - symbol: declarationSymbol + declaration }); } @@ -642,7 +639,7 @@ export class ExportAnalyzer { if (externalModulePath === undefined) { const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol); - return this._getAstNamespaceImport(astModule, declarationSymbol, declaration); + return this._getAstNamespaceImport(astModule, declarationSymbol.name, declaration); } // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for @@ -771,16 +768,15 @@ export class ExportAnalyzer { private _getAstNamespaceImport( astModule: AstModule, - declarationSymbol: ts.Symbol, + localName: string, declaration: ts.Declaration ): AstNamespaceImport { let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule); if (namespaceImport === undefined) { namespaceImport = new AstNamespaceImport({ - namespaceName: declarationSymbol.name, + namespaceName: localName, astModule: astModule, - declaration: declaration, - symbol: declarationSymbol + declaration: declaration }); this._astNamespaceImportByModule.set(astModule, namespaceImport); } diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index a87cba0e609..fb2394ae8cb 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -523,7 +523,7 @@ export class Collector { if (astEntity instanceof AstSymbol) { this._entitiesBySymbol.set(astEntity.followedSymbol, entity); } else if (astEntity instanceof AstNamespaceImport) { - this._entitiesBySymbol.set(astEntity.symbol, entity); + this._entitiesBySymbol.set(astEntity.astModule.moduleSymbol, entity); } this._entities.push(entity); this._collectReferenceDirectives(astEntity); diff --git a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts index 4171f819f94..2dd8e0fa16a 100644 --- a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts +++ b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts @@ -204,15 +204,13 @@ export class DeclarationReferenceGenerator { } if (followedSymbol.flags & ts.SymbolFlags.Alias) { followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol); - - // Without this logic, we end up following the symbol `ns` in `import * as ns from './file'` to - // the actual file `file.ts`. We don't want to do this, so revert to the original symbol. - if (followedSymbol.flags & ts.SymbolFlags.ValueModule) { - followedSymbol = symbol; - } } - if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) { + const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(followedSymbol); + if ( + DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol) && + !(entity?.astEntity instanceof AstNamespaceImport) // skip local namespace import + ) { if (!includeModuleSymbols) { return undefined; } @@ -230,7 +228,6 @@ export class DeclarationReferenceGenerator { } let localName: string = followedSymbol.name; - const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(followedSymbol); if (entity?.nameForEmit) { localName = entity.nameForEmit; } @@ -290,10 +287,8 @@ export class DeclarationReferenceGenerator { firstExportingConsumableParent && firstExportingConsumableParent.astEntity instanceof AstNamespaceImport ) { - const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration( - firstExportingConsumableParent.astEntity.declaration, - this._collector.typeChecker - ); + const parentSymbol: ts.Symbol | undefined = + firstExportingConsumableParent.astEntity.astModule.moduleSymbol; if (parentSymbol) { return this._symbolToDeclarationReference( parentSymbol, diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json index 83b1d9ef718..e1974b396e9 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.json @@ -358,6 +358,36 @@ "isProtected": false, "isAbstract": false }, + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!Item#localModule:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "localModule: " + }, + { + "kind": "Content", + "text": "typeof import('./Options')" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "localModule", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "api-extractor-scenarios!Item#options:member", @@ -374,7 +404,7 @@ { "kind": "Reference", "text": "Options", - "canonicalReference": "api-extractor-scenarios!~Options:interface" + "canonicalReference": "api-extractor-scenarios!~Options_2~Options:interface" }, { "kind": "Content", @@ -514,7 +544,7 @@ { "kind": "Reference", "text": "OptionsClass", - "canonicalReference": "api-extractor-scenarios!~OptionsClass:class" + "canonicalReference": "api-extractor-scenarios!~Options_2~OptionsClass:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md index b2b94eebb56..78242f58c33 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/api-extractor-scenarios.api.md @@ -25,6 +25,10 @@ export class Item { lib2: Lib2Interface; // (undocumented) lib3: Lib1Class; + // Warning: (ae-forgotten-export) The symbol "Options_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + localModule: typeof Options_2; // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts index 64b34b505bc..a68d5772bb1 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType/rollup.d.ts @@ -15,6 +15,7 @@ export declare class Item { lib3: Lib1Class; defaultImport: apiExtractorLib2Test; externalModule: typeof apiExtractorLib3Test; + localModule: typeof Options_2; typeofImportLocal: typeof OptionsClass; typeofImportExternal: typeof Lib1Class; reExportLocal: Lib2Class; @@ -30,6 +31,13 @@ declare interface Options { color: 'red' | 'blue'; } +declare namespace Options_2 { + export { + Options, + OptionsClass + } +} + declare class OptionsClass { } diff --git a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json index 393c60766e6..92c8bcc4753 100644 --- a/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/dynamicImportType2/api-extractor-scenarios.api.json @@ -275,7 +275,7 @@ { "kind": "Reference", "text": "LocalModule.LocalClass", - "canonicalReference": "api-extractor-scenarios!~LocalClass_2:class" + "canonicalReference": "api-extractor-scenarios!~LocalModule~LocalClass_2:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json index 569dc468334..b9c436be1d4 100644 --- a/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json @@ -194,7 +194,7 @@ { "kind": "Reference", "text": "forgottenNs.ForgottenClass", - "canonicalReference": "api-extractor-scenarios!~ForgottenClass:class" + "canonicalReference": "api-extractor-scenarios!~forgottenNs~ForgottenClass:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/etc/exportStarAs2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/exportStarAs2/api-extractor-scenarios.api.json index 3d6aed02798..bfeec33aa0d 100644 --- a/build-tests/api-extractor-scenarios/etc/exportStarAs2/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/exportStarAs2/api-extractor-scenarios.api.json @@ -194,7 +194,7 @@ { "kind": "Reference", "text": "forgottenNs.ForgottenClass", - "canonicalReference": "api-extractor-scenarios!~ForgottenClass:class" + "canonicalReference": "api-extractor-scenarios!~forgottenNs~ForgottenClass:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json index 9a755f8d5cf..ecc2a23d4d0 100644 --- a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json @@ -532,7 +532,7 @@ { "kind": "Reference", "text": "internal2.ForgottenExport6", - "canonicalReference": "api-extractor-scenarios!~ForgottenExport6:class" + "canonicalReference": "api-extractor-scenarios!~internal2~ForgottenExport6:class" }, { "kind": "Content", diff --git a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts index df8be3dc223..cb90bdad480 100644 --- a/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts +++ b/build-tests/api-extractor-scenarios/src/dynamicImportType/Item.ts @@ -9,6 +9,7 @@ export class Item { lib3: import('api-extractor-lib3-test').Lib1Class; defaultImport: import('api-extractor-lib2-test').default; externalModule: typeof import('api-extractor-lib3-test'); + localModule: typeof import('./Options'); typeofImportLocal: typeof import('./Options').OptionsClass; typeofImportExternal: typeof import('api-extractor-lib3-test').Lib1Class; reExportLocal: import('./re-export').Lib2Class; From 0172a6f31b89571f975610963eb8d1d2981f5d12 Mon Sep 17 00:00:00 2001 From: adventure-yunfei Date: Tue, 2 Sep 2025 11:40:42 +0800 Subject: [PATCH 11/11] fix lint --- apps/api-extractor/src/analyzer/AstSubPathImport.ts | 2 +- apps/api-extractor/src/analyzer/AstSymbolTable.ts | 2 +- apps/api-extractor/src/analyzer/ExportAnalyzer.ts | 2 +- apps/api-extractor/src/generators/DtsEmitHelpers.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api-extractor/src/analyzer/AstSubPathImport.ts b/apps/api-extractor/src/analyzer/AstSubPathImport.ts index bf7a622674e..fe27b8e9418 100644 --- a/apps/api-extractor/src/analyzer/AstSubPathImport.ts +++ b/apps/api-extractor/src/analyzer/AstSubPathImport.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import { InternalError } from '@rushstack/node-core-library'; -import { AstEntity, AstSyntheticEntity } from './AstEntity'; +import { type AstEntity, AstSyntheticEntity } from './AstEntity'; export interface IAstSubPathImportOptions { readonly astEntity: AstEntity; diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 1093101f7fe..22e92c19187 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -319,7 +319,7 @@ export class AstSymbolTable { // If this symbol is non-external (i.e. it belongs to the working package), then we also analyze any // referencedAstSymbols that are non-external. For example, this ensures that forgotten exports // get analyzed. - const analyzeReferencedLocalAstEntity = (referencedAstEntity: AstEntity) => { + const analyzeReferencedLocalAstEntity = (referencedAstEntity: AstEntity): void => { if (referencedAstEntity instanceof AstSymbol) { if (!referencedAstEntity.isExternal) { this._analyzeAstSymbol(referencedAstEntity); diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 501ff1f225f..26a58218f7f 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -15,7 +15,7 @@ import type { AstEntity } from './AstEntity'; import { AstNamespaceImport } from './AstNamespaceImport'; import { SyntaxHelpers } from './SyntaxHelpers'; import { AstNamespaceExport } from './AstNamespaceExport'; -import { AstSubPathImport, IAstSubPathImportOptions } from './AstSubPathImport'; +import { AstSubPathImport, type IAstSubPathImportOptions } from './AstSubPathImport'; /** * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 7c61434ac40..3e16f928b5f 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; import { InternalError } from '@rushstack/node-core-library'; import type { CollectorEntity } from '../collector/CollectorEntity'; -import { AstImport, AstImportKind } from '../analyzer/AstImport'; +import { type AstImport, AstImportKind } from '../analyzer/AstImport'; import { AstDeclaration } from '../analyzer/AstDeclaration'; import type { Collector } from '../collector/Collector'; import type { Span } from '../analyzer/Span';