diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index e59d7603ba6..3e8c6d1cd34 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -304,7 +304,16 @@ export class ApiReportGenerator { case ts.SyntaxKind.ExportKeyword: case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: - // Delete any explicit "export" or "declare" keywords -- we will re-add them below + // Don't remove the export keyword from `export { ... }` declarations + if ( + span.parent?.node && + ts.isExportDeclaration(span.parent.node) && + span.parent.node.exportClause?.kind === ts.SyntaxKind.NamedExports + ) { + break; + } + + // Delete other explicit "export" or "declare" keywords -- we will re-add them below span.modification.skipAll(); break; @@ -315,6 +324,11 @@ export class ApiReportGenerator { case ts.SyntaxKind.ModuleKeyword: case ts.SyntaxKind.TypeKeyword: case ts.SyntaxKind.FunctionKeyword: + // Don't touch `type` keywords inside `export { ... }` declarations + if (span.node.parent.kind === ts.SyntaxKind.ExportSpecifier) { + break; + } + // Replace the stuff we possibly deleted above let replacedModifiers: string = ''; @@ -399,6 +413,33 @@ export class ApiReportGenerator { insideTypeLiteral = true; break; + case ts.SyntaxKind.ExportSpecifier: + recurseChildren = false; + const node: ts.ExportSpecifier = span.node as ts.ExportSpecifier; + const localName: ts.ModuleName = node.propertyName ?? node.name; + const exportedName: ts.ModuleName = node.name; + let exportedSymbol: ts.Symbol | undefined = collector.typeChecker.getSymbolAtLocation(localName); + // eslint-disable-next-line no-bitwise + if (exportedSymbol && exportedSymbol.flags & ts.SymbolFlags.Alias) { + exportedSymbol = collector.typeChecker.getAliasedSymbol(exportedSymbol); + } + if (exportedSymbol) { + const exportEntity: CollectorEntity | undefined = collector.tryGetEntityForSymbol(exportedSymbol); + if (exportEntity && exportEntity.nameForEmit && localName.getText() !== exportEntity.nameForEmit) { + const nameSpan: Span | undefined = span.children.find((e) => e.node === localName); + if (nameSpan) { + if (exportedName === localName) { + nameSpan.modification.skipAll(); + nameSpan.modification.prefix = exportEntity.nameForEmit + ' as ' + nameSpan.getText(); + } else { + nameSpan.modification.skipAll(); + nameSpan.modification.prefix = exportEntity.nameForEmit + ' '; + } + } + } + } + break; + case ts.SyntaxKind.ImportType: DtsEmitHelpers.modifyImportTypeSpan( collector, diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index baa933c9440..4983affddd8 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -260,7 +260,7 @@ export class DtsRollupGenerator { switch (span.kind) { case ts.SyntaxKind.JSDocComment: // If the @packageDocumentation comment seems to be attached to one of the regular API items, - // omit it. It gets explictly emitted at the top of the file. + // omit it. It gets explicitly emitted at the top of the file. if (span.node.getText().match(/(?:\s|\*)@packageDocumentation(?:\s|\*)/gi)) { span.modification.skipAll(); } @@ -272,7 +272,16 @@ export class DtsRollupGenerator { case ts.SyntaxKind.ExportKeyword: case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: - // Delete any explicit "export" or "declare" keywords -- we will re-add them below + // Don't remove the export keyword from `export { ... }` declarations + if ( + span.parent?.node && + ts.isExportDeclaration(span.parent.node) && + span.parent.node.exportClause?.kind === ts.SyntaxKind.NamedExports + ) { + break; + } + + // Delete other explicit "export" or "declare" keywords -- we will re-add them below span.modification.skipAll(); break; @@ -283,6 +292,11 @@ export class DtsRollupGenerator { case ts.SyntaxKind.ModuleKeyword: case ts.SyntaxKind.TypeKeyword: case ts.SyntaxKind.FunctionKeyword: + // Don't touch `type` keywords inside `export { ... }` declarations + if (span.node.parent.kind === ts.SyntaxKind.ExportSpecifier) { + break; + } + // Replace the stuff we possibly deleted above let replacedModifiers: string = ''; @@ -372,6 +386,33 @@ export class DtsRollupGenerator { } break; + case ts.SyntaxKind.ExportSpecifier: + recurseChildren = false; + const node: ts.ExportSpecifier = span.node as ts.ExportSpecifier; + const localName: ts.ModuleName = node.propertyName ?? node.name; + const exportedName: ts.ModuleName = node.name; + let exportedSymbol: ts.Symbol | undefined = collector.typeChecker.getSymbolAtLocation(localName); + // eslint-disable-next-line no-bitwise + if (exportedSymbol && exportedSymbol.flags & ts.SymbolFlags.Alias) { + exportedSymbol = collector.typeChecker.getAliasedSymbol(exportedSymbol); + } + if (exportedSymbol) { + const exportEntity: CollectorEntity | undefined = collector.tryGetEntityForSymbol(exportedSymbol); + if (exportEntity && exportEntity.nameForEmit && localName.getText() !== exportEntity.nameForEmit) { + const nameSpan: Span | undefined = span.children.find((e) => e.node === localName); + if (nameSpan) { + if (exportedName === localName) { + nameSpan.modification.skipAll(); + nameSpan.modification.prefix = exportEntity.nameForEmit + ' as ' + nameSpan.getText(); + } else { + nameSpan.modification.skipAll(); + nameSpan.modification.prefix = exportEntity.nameForEmit + ' '; + } + } + } + } + break; + case ts.SyntaxKind.ImportType: DtsEmitHelpers.modifyImportTypeSpan( collector, diff --git a/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.json index 64b88b41b1e..5e86c3b5d5b 100644 --- a/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.json @@ -559,6 +559,46 @@ } ] }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!n2:namespace", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare namespace n2 " + } + ], + "fileUrlPath": "src/apiItemKinds/namespaces.ts", + "releaseTag": "Public", + "name": "n2", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Variable", + "canonicalReference": "api-extractor-scenarios!n2.name2:var", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "name2: " + }, + { + "kind": "Reference", + "text": "SomeType", + "canonicalReference": "api-extractor-scenarios!SomeOtherType:type" + } + ], + "isReadonly": true, + "releaseTag": "Public", + "name": "name2", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + } + ] + }, { "kind": "Variable", "canonicalReference": "api-extractor-scenarios!nonConstVariable:var", @@ -912,6 +952,32 @@ "parameters": [], "name": "someFunction" }, + { + "kind": "TypeAlias", + "canonicalReference": "api-extractor-scenarios!SomeOtherType:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "type SomeType = " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/apiItemKinds/namespaces.ts", + "releaseTag": "Public", + "name": "SomeOtherType", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "TypeAlias", "canonicalReference": "api-extractor-scenarios!SomeType:type", diff --git a/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.md index c0743f1c37e..0cf156f446f 100644 --- a/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/apiItemKinds/api-extractor-scenarios.api.md @@ -51,7 +51,7 @@ export namespace n1 { // (undocumented) export class SomeClass2 extends SomeClass1 { } - {}; + export {}; } // @public (undocumented) @@ -61,6 +61,13 @@ export namespace n1 { } } +// @public (undocumented) +export namespace n2 { + const // (undocumented) + name2: SomeOtherType; + export { SomeOtherType as SomeType, type SomeOtherType as YetAnotherType, type name2 }; +} + // @public (undocumented) export let nonConstVariable: string; @@ -91,6 +98,9 @@ export class SimpleClass { // @public (undocumented) export function someFunction(): void; +// @public (undocumented) +export type SomeOtherType = string; + // @public (undocumented) export type SomeType = number; diff --git a/build-tests/api-extractor-scenarios/etc/apiItemKinds/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/apiItemKinds/rollup.d.ts index dbe1c3219c1..ff798063d8a 100644 --- a/build-tests/api-extractor-scenarios/etc/apiItemKinds/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/apiItemKinds/rollup.d.ts @@ -36,7 +36,7 @@ export declare namespace n1 { export class SomeClass3 { } } - {}; + export {}; } /** @public */ @@ -45,6 +45,12 @@ export declare namespace n1 { } } +/** @public */ +export declare namespace n2 { + const name2: SomeOtherType; + export { SomeOtherType as SomeType, type SomeOtherType as YetAnotherType, type name2 }; +} + /** @public */ export declare let nonConstVariable: string; @@ -78,6 +84,9 @@ export declare class SimpleClass { /** @public */ export declare function someFunction(): void; +/** @public */ +export declare type SomeOtherType = string; + /** @public */ export declare type SomeType = number; diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md index 43157fe55bb..483dafd6381 100644 --- a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md @@ -86,7 +86,7 @@ export namespace SomeNamespace1 { } // (undocumented) export function someFunction3(): ForgottenExport3; - {}; + export {}; } // (No @packageDocumentation comment for this package) diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts index 8010e499625..4d56859ed89 100644 --- a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts @@ -80,7 +80,7 @@ export declare namespace SomeNamespace1 { export class ForgottenExport3 { } export function someFunction3(): ForgottenExport3; - {}; + export {}; } export { } diff --git a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md index 065b8df0b85..31940327106 100644 --- a/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/referenceTokens/api-extractor-scenarios.api.md @@ -21,13 +21,13 @@ export namespace n1 { export function someFunction2(): SomeType2; // (undocumented) export type SomeType2 = number; - {}; + export {}; } // (undocumented) export function someFunction1(): SomeType1; // (undocumented) export type SomeType1 = number; - {}; + export {}; } // @public (undocumented) diff --git a/build-tests/api-extractor-scenarios/etc/referenceTokens/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/referenceTokens/rollup.d.ts index d1e60042702..c27d5c3ee90 100644 --- a/build-tests/api-extractor-scenarios/etc/referenceTokens/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/referenceTokens/rollup.d.ts @@ -14,9 +14,9 @@ export declare namespace n1 { export type SomeType3 = number; export function someFunction3(): n2.n3.SomeType3; } - {}; + export {}; } - {}; + export {}; } /** @public */ diff --git a/build-tests/api-extractor-scenarios/src/apiItemKinds/namespaces.ts b/build-tests/api-extractor-scenarios/src/apiItemKinds/namespaces.ts index 33513d36b1f..fd115aa7b89 100644 --- a/build-tests/api-extractor-scenarios/src/apiItemKinds/namespaces.ts +++ b/build-tests/api-extractor-scenarios/src/apiItemKinds/namespaces.ts @@ -12,3 +12,13 @@ export namespace n1 { export namespace n1 { export class SomeClass4 {} } + +/** @public */ +type SomeType = string; +export { SomeType as SomeOtherType }; + +/** @public */ +export declare namespace n2 { + const name2: SomeType; + export { SomeType, type SomeType as YetAnotherType, type name2 }; +} diff --git a/common/changes/@microsoft/api-extractor/fix-namespaces_2025-10-31-20-03.json b/common/changes/@microsoft/api-extractor/fix-namespaces_2025-10-31-20-03.json new file mode 100644 index 00000000000..01c0045f5d7 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/fix-namespaces_2025-10-31-20-03.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Properly support ESM-style `export { ... }` declarations under `declare namespace`.", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file