From e8f354e8bcb1369836c627b908a8032117f8eb2a Mon Sep 17 00:00:00 2001 From: darshpatel1052 <147577241+darshpatel1052@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:29:23 +0530 Subject: [PATCH] fix(api-extractor): preserve namespace import-equals exports --- .../src/generators/DtsEmitHelpers.ts | 8 +- .../api-extractor-scenarios.api.json | 301 ++++++++++++++++++ .../api-extractor-scenarios.api.md | 29 ++ .../etc/namespaceImportEquals/rollup.d.ts | 18 ++ .../src/namespaceImportEquals/index.ts | 20 ++ .../src/runScenarios.ts | 26 ++ .../test/namespaceImportEquals/consumer.ts | 7 + .../test/namespaceImportEquals/tsconfig.json | 10 + ...espace-import-equals_2026-11-03-06-00.json | 11 + 9 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.json create mode 100644 build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.md create mode 100644 build-tests/api-extractor-scenarios/etc/namespaceImportEquals/rollup.d.ts create mode 100644 build-tests/api-extractor-scenarios/src/namespaceImportEquals/index.ts create mode 100644 build-tests/api-extractor-scenarios/test/namespaceImportEquals/consumer.ts create mode 100644 build-tests/api-extractor-scenarios/test/namespaceImportEquals/tsconfig.json create mode 100644 common/changes/@microsoft/api-extractor/fix-namespace-import-equals_2026-11-03-06-00.json diff --git a/apps/api-extractor/src/generators/DtsEmitHelpers.ts b/apps/api-extractor/src/generators/DtsEmitHelpers.ts index 708ddd27f38..4307c6c3daf 100644 --- a/apps/api-extractor/src/generators/DtsEmitHelpers.ts +++ b/apps/api-extractor/src/generators/DtsEmitHelpers.ts @@ -189,12 +189,12 @@ export class DtsEmitHelpers { } /** - * Checks if an export keyword is part of an ExportDeclaration inside a namespace - * (e.g., "export { Foo, Bar };" inside "declare namespace SDK { ... }"). - * In that case, the export keyword must be preserved, otherwise the output is invalid TypeScript. + * Checks if an export keyword is part of an ExportDeclaration or ImportEqualsDeclaration inside a namespace + * (e.g., "export { Foo, Bar };" or "export import Foo = Bar.Foo;" inside "declare namespace SDK { ... }"). + * In these cases, the export keyword must be preserved so the declaration remains valid and exported. */ public static isExportKeywordInNamespaceExportDeclaration(node: ts.Node): boolean { - if (node.parent && ts.isExportDeclaration(node.parent)) { + if (node.parent && (ts.isExportDeclaration(node.parent) || ts.isImportEqualsDeclaration(node.parent))) { const moduleBlock: ts.ModuleBlock | undefined = TypeScriptHelpers.findFirstParent( node, ts.SyntaxKind.ModuleBlock diff --git a/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..c4808ec86d5 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.json @@ -0,0 +1,301 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1011, + "oldestForwardsCompatibleVersion": 1001, + "tsdocConfig": { + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "noStandardTags": true, + "tagDefinitions": [ + { + "tagName": "@alpha", + "syntaxKind": "modifier" + }, + { + "tagName": "@beta", + "syntaxKind": "modifier" + }, + { + "tagName": "@defaultValue", + "syntaxKind": "block" + }, + { + "tagName": "@decorator", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@deprecated", + "syntaxKind": "block" + }, + { + "tagName": "@eventProperty", + "syntaxKind": "modifier" + }, + { + "tagName": "@example", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@experimental", + "syntaxKind": "modifier" + }, + { + "tagName": "@inheritDoc", + "syntaxKind": "inline" + }, + { + "tagName": "@internal", + "syntaxKind": "modifier" + }, + { + "tagName": "@label", + "syntaxKind": "inline" + }, + { + "tagName": "@link", + "syntaxKind": "inline", + "allowMultiple": true + }, + { + "tagName": "@override", + "syntaxKind": "modifier" + }, + { + "tagName": "@packageDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@param", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@privateRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@public", + "syntaxKind": "modifier" + }, + { + "tagName": "@readonly", + "syntaxKind": "modifier" + }, + { + "tagName": "@remarks", + "syntaxKind": "block" + }, + { + "tagName": "@returns", + "syntaxKind": "block" + }, + { + "tagName": "@sealed", + "syntaxKind": "modifier" + }, + { + "tagName": "@see", + "syntaxKind": "block" + }, + { + "tagName": "@throws", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@typeParam", + "syntaxKind": "block", + "allowMultiple": true + }, + { + "tagName": "@virtual", + "syntaxKind": "modifier" + }, + { + "tagName": "@jsx", + "syntaxKind": "block" + }, + { + "tagName": "@jsxRuntime", + "syntaxKind": "block" + }, + { + "tagName": "@jsxFrag", + "syntaxKind": "block" + }, + { + "tagName": "@jsxImportSource", + "syntaxKind": "block" + }, + { + "tagName": "@betaDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@internalRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@preapproved", + "syntaxKind": "modifier" + } + ], + "supportForTags": { + "@alpha": true, + "@beta": true, + "@defaultValue": true, + "@decorator": true, + "@deprecated": true, + "@eventProperty": true, + "@example": true, + "@experimental": true, + "@inheritDoc": true, + "@internal": true, + "@label": true, + "@link": true, + "@override": true, + "@packageDocumentation": true, + "@param": true, + "@privateRemarks": true, + "@public": true, + "@readonly": true, + "@remarks": true, + "@returns": true, + "@sealed": true, + "@see": true, + "@throws": true, + "@typeParam": true, + "@virtual": true, + "@betaDocumentation": true, + "@internalRemarks": true, + "@preapproved": true + }, + "reportUnsupportedHtmlElements": false + } + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!Camera:namespace", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare namespace Camera " + } + ], + "fileUrlPath": "src/namespaceImportEquals/index.ts", + "releaseTag": "Public", + "name": "Camera", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Enum", + "canonicalReference": "api-extractor-scenarios!Camera.TransitionType:enum", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "enum TransitionType " + } + ], + "releaseTag": "Public", + "name": "TransitionType", + "preserveMemberOrder": false, + "members": [ + { + "kind": "EnumMember", + "canonicalReference": "api-extractor-scenarios!Camera.TransitionType.FLY:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "FLY = " + }, + { + "kind": "Content", + "text": "\"fly\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "FLY" + }, + { + "kind": "EnumMember", + "canonicalReference": "api-extractor-scenarios!Camera.TransitionType.INSTANT:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "INSTANT = " + }, + { + "kind": "Content", + "text": "\"instant\"" + } + ], + "initializerTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "name": "INSTANT" + } + ] + } + ] + }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!Sweep:namespace", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare namespace Sweep " + } + ], + "fileUrlPath": "src/namespaceImportEquals/index.ts", + "releaseTag": "Public", + "name": "Sweep", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!Sweep.Nested:namespace", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "namespace Nested " + } + ], + "releaseTag": "Public", + "name": "Nested", + "preserveMemberOrder": false, + "members": [] + } + ] + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..ffbbc1d3ed8 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/api-extractor-scenarios.api.md @@ -0,0 +1,29 @@ +## API Report File for "api-extractor-scenarios" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export namespace Camera { + // (undocumented) + export enum TransitionType { + // (undocumented) + FLY = "fly", + // (undocumented) + INSTANT = "instant" + } +} + +// @public (undocumented) +export namespace Sweep { + export import Transition = Camera.TransitionType; + // (undocumented) + export namespace Nested { + export import Transition = Camera.TransitionType; + } +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/rollup.d.ts new file mode 100644 index 00000000000..6df93c6708d --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/namespaceImportEquals/rollup.d.ts @@ -0,0 +1,18 @@ +/** @public */ +export declare namespace Camera { + export enum TransitionType { + FLY = "fly", + INSTANT = "instant" + } +} + +/** @public */ +export declare namespace Sweep { + /** Alias kept for backward compatibility. */ + export import Transition = Camera.TransitionType; + export namespace Nested { + export import Transition = Camera.TransitionType; + } +} + +export { } diff --git a/build-tests/api-extractor-scenarios/src/namespaceImportEquals/index.ts b/build-tests/api-extractor-scenarios/src/namespaceImportEquals/index.ts new file mode 100644 index 00000000000..687145f843e --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/namespaceImportEquals/index.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** @public */ +export namespace Camera { + export enum TransitionType { + FLY = 'fly', + INSTANT = 'instant' + } +} + +/** @public */ +export namespace Sweep { + /** Alias kept for backward compatibility. */ + export import Transition = Camera.TransitionType; + + export namespace Nested { + export import Transition = Camera.TransitionType; + } +} diff --git a/build-tests/api-extractor-scenarios/src/runScenarios.ts b/build-tests/api-extractor-scenarios/src/runScenarios.ts index 6282ce60443..3ed461b3e2d 100644 --- a/build-tests/api-extractor-scenarios/src/runScenarios.ts +++ b/build-tests/api-extractor-scenarios/src/runScenarios.ts @@ -2,11 +2,37 @@ // See LICENSE in the project root for license information. import type { IRunScriptOptions } from '@rushstack/heft'; +import { Executable, Import } from '@rushstack/node-core-library'; import { runScenariosAsync } from 'run-scenarios-helpers'; export async function runAsync(runScriptOptions: IRunScriptOptions): Promise { await runScenariosAsync(runScriptOptions, { libFolderPath: __dirname, + afterApiExtractorAsync: async (scenarioFolderName: string) => { + if (scenarioFolderName === 'namespaceImportEquals') { + const { buildFolderPath } = runScriptOptions.heftConfiguration; + // Compile a consumer against the generated rollup using API Extractor's TypeScript version. + const typescriptCompilerPath: string = Import.resolveModule({ + modulePath: 'typescript/bin/tsc', + baseFolderPath: Import.resolvePackage({ + packageName: '@microsoft/api-extractor', + baseFolderPath: __dirname + }) + }); + await Executable.waitForExitAsync( + Executable.spawn( + process.execPath, + [ + typescriptCompilerPath, + '--project', + `${buildFolderPath}/test/namespaceImportEquals/tsconfig.json` + ], + { stdio: 'inherit' } + ), + { throwOnNonZeroExitCode: true, throwOnSignal: true } + ); + } + }, additionalApiExtractorConfig: { messages: { extractorMessageReporting: { diff --git a/build-tests/api-extractor-scenarios/test/namespaceImportEquals/consumer.ts b/build-tests/api-extractor-scenarios/test/namespaceImportEquals/consumer.ts new file mode 100644 index 00000000000..42e75f70258 --- /dev/null +++ b/build-tests/api-extractor-scenarios/test/namespaceImportEquals/consumer.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Sweep } from '../../temp/etc/namespaceImportEquals/rollup'; + +export const transition: Sweep.Transition = Sweep.Transition.FLY; +export const nestedTransition: Sweep.Nested.Transition = Sweep.Nested.Transition.INSTANT; diff --git a/build-tests/api-extractor-scenarios/test/namespaceImportEquals/tsconfig.json b/build-tests/api-extractor-scenarios/test/namespaceImportEquals/tsconfig.json new file mode 100644 index 00000000000..e298f72e5cc --- /dev/null +++ b/build-tests/api-extractor-scenarios/test/namespaceImportEquals/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "strict": true, + "noEmit": true, + "types": [] + }, + "files": ["consumer.ts"] +} diff --git a/common/changes/@microsoft/api-extractor/fix-namespace-import-equals_2026-11-03-06-00.json b/common/changes/@microsoft/api-extractor/fix-namespace-import-equals_2026-11-03-06-00.json new file mode 100644 index 00000000000..abaab7c9e0d --- /dev/null +++ b/common/changes/@microsoft/api-extractor/fix-namespace-import-equals_2026-11-03-06-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Preserve the export keyword for import-equals aliases inside namespaces in declaration rollups and API reports.", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "147577241+darshpatel1052@users.noreply.github.com" +}