From 4f4d627ad8546e9396fb87fba6caadd753e57d47 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Fri, 29 May 2026 16:53:37 +0200 Subject: [PATCH 1/2] [api-extractor] Fix internal error when importing a re-exported namespace by name --- .../src/analyzer/ExportAnalyzer.ts | 31 ++- .../src/Lib2ReexportedNamespace.ts | 7 + .../src/Lib2ReexportedNamespaceTarget.ts | 7 + .../api-extractor-scenarios.api.json | 223 ++++++++++++++++++ .../api-extractor-scenarios.api.md | 14 ++ .../rollup.d.ts | 6 + .../index.ts | 15 ++ ...pace-reexport-import_2026-05-29-16-45.json | 11 + 8 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespace.ts create mode 100644 build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespaceTarget.ts create mode 100644 build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.json create mode 100644 build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.md create mode 100644 build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/rollup.d.ts create mode 100644 build-tests/api-extractor-scenarios/src/importExternalReexportedNamespace/index.ts create mode 100644 common/changes/@microsoft/api-extractor/fix-namespace-reexport-import_2026-05-29-16-45.json diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 0bc6236aa4d..fda5de41308 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -977,12 +977,21 @@ export class ExportAnalyzer { if (importSymbol) { const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(importSymbol, this._typeChecker); - astImport.astSymbol = this._astSymbolTable.fetchAstSymbol({ - followedSymbol: followedSymbol, - isExternal: true, - includeNominalAnalysis: false, - addIfMissing: true - }); + // If the import resolves to an entire module rather than a declaration within a module, then there + // is no AstSymbol to associate with it. This happens when a dependency re-exports a module as a + // namespace (e.g. `export * as ns from './module'`) and we import that namespace by name. Following + // the alias chain lands on the module's source file symbol, which API Extractor cannot represent as + // an AstSymbol (a SourceFile is not a supported declaration kind). This mirrors the handling of + // `import * as ns from '...'` star imports, where `importSymbol` is undefined. + // See https://github.com/microsoft/rushstack/issues/4963 + if (!ExportAnalyzer._isExternalModuleSymbol(followedSymbol)) { + astImport.astSymbol = this._astSymbolTable.fetchAstSymbol({ + followedSymbol: followedSymbol, + isExternal: true, + includeNominalAnalysis: false, + addIfMissing: true + }); + } } } else { // If we encounter at least one import that does not use the type-only form, @@ -1011,4 +1020,14 @@ export class ExportAnalyzer { return moduleSpecifier; } + + /** + * Returns true if the symbol represents an entire module (i.e. its declaration is a source file), + * as opposed to a declaration appearing within a module. + */ + private static _isExternalModuleSymbol(symbol: ts.Symbol): boolean { + // eslint-disable-next-line no-bitwise + const isValueModule: boolean = !!(symbol.flags & ts.SymbolFlags.ValueModule); + return isValueModule && symbol.valueDeclaration !== undefined && ts.isSourceFile(symbol.valueDeclaration); + } } diff --git a/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespace.ts b/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespace.ts new file mode 100644 index 00000000000..eaee54465d3 --- /dev/null +++ b/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespace.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. + +// Re-exports an entire module as a namespace. When a downstream package imports this namespace by +// name and references a type within it, following the alias chain lands on this module's source file +// symbol. See https://github.com/microsoft/rushstack/issues/4963 +export * as Lib2ReexportedNamespace from './Lib2ReexportedNamespaceTarget'; diff --git a/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespaceTarget.ts b/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespaceTarget.ts new file mode 100644 index 00000000000..3075e782503 --- /dev/null +++ b/build-tests/api-extractor-lib2-test/src/Lib2ReexportedNamespaceTarget.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. + +/** @public */ +export interface Lib2ReexportedInterface { + prop: number; +} diff --git a/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..eb53131f4d0 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.json @@ -0,0 +1,223 @@ +{ + "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": "Function", + "canonicalReference": "api-extractor-scenarios!useReexportedNamespace:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function useReexportedNamespace(): " + }, + { + "kind": "Reference", + "text": "Lib2ReexportedNamespace.Lib2ReexportedInterface", + "canonicalReference": "api-extractor-lib2-test!Lib2ReexportedInterface:interface" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "src/importExternalReexportedNamespace/index.ts", + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "useReexportedNamespace" + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..2f86b06f662 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/api-extractor-scenarios.api.md @@ -0,0 +1,14 @@ +## 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 + +import { Lib2ReexportedNamespace } from 'api-extractor-lib2-test/lib/Lib2ReexportedNamespace'; + +// @public (undocumented) +export function useReexportedNamespace(): Lib2ReexportedNamespace.Lib2ReexportedInterface; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/rollup.d.ts new file mode 100644 index 00000000000..c7af61508b0 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/importExternalReexportedNamespace/rollup.d.ts @@ -0,0 +1,6 @@ +import { Lib2ReexportedNamespace } from 'api-extractor-lib2-test/lib/Lib2ReexportedNamespace'; + +/** @public */ +export declare function useReexportedNamespace(): Lib2ReexportedNamespace.Lib2ReexportedInterface; + +export { } diff --git a/build-tests/api-extractor-scenarios/src/importExternalReexportedNamespace/index.ts b/build-tests/api-extractor-scenarios/src/importExternalReexportedNamespace/index.ts new file mode 100644 index 00000000000..6f16241f0e5 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/importExternalReexportedNamespace/index.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// Regression test for https://github.com/microsoft/rushstack/issues/4963 +// +// `Lib2ReexportedNamespace` is imported by name from an external package, but in that package it was +// produced by re-exporting an entire module as a namespace (`export * as Lib2ReexportedNamespace from +// './...'`). Referencing a type within it used to crash API Extractor with an internal error, because +// following the alias chain lands on the module's source file symbol. +import { Lib2ReexportedNamespace } from 'api-extractor-lib2-test/lib/Lib2ReexportedNamespace'; + +/** @public */ +export function useReexportedNamespace(): Lib2ReexportedNamespace.Lib2ReexportedInterface { + return { prop: 1 }; +} diff --git a/common/changes/@microsoft/api-extractor/fix-namespace-reexport-import_2026-05-29-16-45.json b/common/changes/@microsoft/api-extractor/fix-namespace-reexport-import_2026-05-29-16-45.json new file mode 100644 index 00000000000..6d2011c5817 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/fix-namespace-reexport-import_2026-05-29-16-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix an internal error (\"... symbol has a ts.SyntaxKind.SourceFile declaration which is not (yet?) supported by API Extractor\") that occurred when importing by name a namespace that a dependency had produced by re-exporting an entire module (e.g. `export * as ns from './module'`) and then referencing a type within it.", + "type": "patch", + "packageName": "@microsoft/api-extractor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "lukas@livekit.io" +} From 224c23dd04e0d4fd9e6ca25a0b66071c7d900c91 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Fri, 29 May 2026 17:32:14 +0200 Subject: [PATCH 2/2] consolidate helper --- apps/api-extractor/src/analyzer/ExportAnalyzer.ts | 12 +----------- apps/api-extractor/src/analyzer/TypeScriptHelpers.ts | 12 ++++++++++++ .../src/generators/DeclarationReferenceGenerator.ts | 12 ++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index fda5de41308..13bba49cc40 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -984,7 +984,7 @@ export class ExportAnalyzer { // an AstSymbol (a SourceFile is not a supported declaration kind). This mirrors the handling of // `import * as ns from '...'` star imports, where `importSymbol` is undefined. // See https://github.com/microsoft/rushstack/issues/4963 - if (!ExportAnalyzer._isExternalModuleSymbol(followedSymbol)) { + if (!TypeScriptHelpers.isExternalModuleSymbol(followedSymbol)) { astImport.astSymbol = this._astSymbolTable.fetchAstSymbol({ followedSymbol: followedSymbol, isExternal: true, @@ -1020,14 +1020,4 @@ export class ExportAnalyzer { return moduleSpecifier; } - - /** - * Returns true if the symbol represents an entire module (i.e. its declaration is a source file), - * as opposed to a declaration appearing within a module. - */ - private static _isExternalModuleSymbol(symbol: ts.Symbol): boolean { - // eslint-disable-next-line no-bitwise - const isValueModule: boolean = !!(symbol.flags & ts.SymbolFlags.ValueModule); - return isValueModule && symbol.valueDeclaration !== undefined && ts.isSourceFile(symbol.valueDeclaration); - } } diff --git a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts index 5c28b30d354..2fde26778ea 100644 --- a/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts +++ b/apps/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -75,6 +75,18 @@ export class TypeScriptHelpers { return undefined; } + /** + * Returns true if the symbol represents an entire module (i.e. its declaration is a source file), + * as opposed to a declaration appearing within a module. + */ + public static isExternalModuleSymbol(symbol: ts.Symbol): boolean { + return ( + !!(symbol.flags & ts.SymbolFlags.ValueModule) && + symbol.valueDeclaration !== undefined && + ts.isSourceFile(symbol.valueDeclaration) + ); + } + /** * Returns true if the specified symbol is an ambient declaration. */ diff --git a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts index 6f136fb9829..a1c064191ce 100644 --- a/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts +++ b/apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts @@ -71,14 +71,6 @@ export class DeclarationReferenceGenerator { } } - private static _isExternalModuleSymbol(symbol: ts.Symbol): boolean { - return ( - !!(symbol.flags & ts.SymbolFlags.ValueModule) && - symbol.valueDeclaration !== undefined && - ts.isSourceFile(symbol.valueDeclaration) - ); - } - private static _isSameSymbol(left: ts.Symbol | undefined, right: ts.Symbol): boolean { return ( left === right || @@ -125,7 +117,7 @@ export class DeclarationReferenceGenerator { // If its parent symbol is not a source file, then use either Exports or Members. If the parent symbol // is a source file, but it wasn't exported from the package entry point (in the check above), then the // symbol is a local, so fall through below. - if (parent && !DeclarationReferenceGenerator._isExternalModuleSymbol(parent)) { + if (parent && !TypeScriptHelpers.isExternalModuleSymbol(parent)) { if ( parent.members && DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol) @@ -214,7 +206,7 @@ export class DeclarationReferenceGenerator { } } - if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) { + if (TypeScriptHelpers.isExternalModuleSymbol(followedSymbol)) { if (!includeModuleSymbols) { return undefined; }