diff --git a/apps/api-extractor/.vscode/launch.json b/apps/api-extractor/.vscode/launch.json index f81af6c2ef3..2a4aa769b6b 100644 --- a/apps/api-extractor/.vscode/launch.json +++ b/apps/api-extractor/.vscode/launch.json @@ -74,7 +74,13 @@ "name": "scenario", "program": "${workspaceFolder}/lib/start.js", "cwd": "${workspaceFolder}/../../build-tests/api-extractor-scenarios", - "args": ["--debug", "run", "--local", "--config", "./temp/configs/api-extractor-typeof.json"], + "args": [ + "--debug", + "run", + "--local", + "--config", + "./temp/configs/api-extractor-exportImportStarAs2.json" + ], "sourceMaps": true } ] diff --git a/apps/api-extractor/src/analyzer/AstDeclaration.ts b/apps/api-extractor/src/analyzer/AstDeclaration.ts index f3f8eba58dd..02540312168 100644 --- a/apps/api-extractor/src/analyzer/AstDeclaration.ts +++ b/apps/api-extractor/src/analyzer/AstDeclaration.ts @@ -5,7 +5,7 @@ import * as ts from 'typescript'; import { AstSymbol } from './AstSymbol'; import { Span } from './Span'; import { InternalError } from '@rushstack/node-core-library'; -import { AstEntity } from './AstSymbolTable'; +import { AstEntity } from './AstEntity'; /** * Constructor options for AstDeclaration diff --git a/apps/api-extractor/src/analyzer/AstEntity.ts b/apps/api-extractor/src/analyzer/AstEntity.ts new file mode 100644 index 00000000000..b3c6ceeda1b --- /dev/null +++ b/apps/api-extractor/src/analyzer/AstEntity.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * `AstEntity` is the abstract base class for analyzer objects that can become a `CollectorEntity`. + * + * @remarks + * + * The subclasses are: + * ``` + * - AstEntity + * - AstSymbol + * - AstSyntheticEntity + * - AstImport + * - AstNamespaceImport + * ``` + */ +export abstract class AstEntity { + /** + * The original name of the symbol, as exported from the module (i.e. source file) + * containing the original TypeScript definition. Constructs such as + * `import { X as Y } from` may introduce other names that differ from the local name. + * + * @remarks + * For the most part, `localName` corresponds to `followedSymbol.name`, but there + * are some edge cases. For example, the ts.Symbol.name for `export default class X { }` + * is actually `"default"`, not `"X"`. + */ + public abstract readonly localName: string; +} + +/** + * `AstSyntheticEntity` is the abstract base class for analyzer objects whose emitted declarations + * are not text transformations performed by the `Span` helper. + * + * @remarks + * Most of API Extractor's output is produced by using the using the `Span` utility to regurgitate strings from + * the input .d.ts files. If we need to rename an identifier, the `Span` visitor can pick out an interesting + * node and rewrite its string, but otherwise the transformation operates on dumb text and not compiler concepts. + * (Historically we did this because the compiler's emitter was an internal API, but it still has some advantages, + * for example preserving syntaxes generated by an older compiler to avoid incompatibilities.) + * + * This strategy does not work for cases where the output looks very different from the input. Today these + * cases are always kinds of `import` statements, but that may change in the future. + */ +export abstract class AstSyntheticEntity extends AstEntity {} diff --git a/apps/api-extractor/src/analyzer/AstImport.ts b/apps/api-extractor/src/analyzer/AstImport.ts index 92454ee4e2b..13998d92dd9 100644 --- a/apps/api-extractor/src/analyzer/AstImport.ts +++ b/apps/api-extractor/src/analyzer/AstImport.ts @@ -3,6 +3,7 @@ import { AstSymbol } from './AstSymbol'; import { InternalError } from '@rushstack/node-core-library'; +import { AstSyntheticEntity } from './AstEntity'; /** * Indicates the import kind for an `AstImport`. @@ -49,7 +50,7 @@ export interface IAstImportOptions { * For a symbol that was imported from an external package, this tracks the import * statement that was used to reach it. */ -export class AstImport { +export class AstImport extends AstSyntheticEntity { public readonly importKind: AstImportKind; /** @@ -110,6 +111,8 @@ export class AstImport { public readonly key: string; public constructor(options: IAstImportOptions) { + super(); + this.importKind = options.importKind; this.modulePath = options.modulePath; this.exportName = options.exportName; @@ -120,11 +123,9 @@ export class AstImport { this.key = AstImport.getKey(options); } - /** - * Allows `AstEntity.localName` to be used as a convenient generalization of `AstSymbol.localName` and - * `AstImport.exportName`. - */ + /** {@inheritdoc} */ public get localName(): string { + // abstract return this.exportName; } diff --git a/apps/api-extractor/src/analyzer/AstModule.ts b/apps/api-extractor/src/analyzer/AstModule.ts index f18efb60a42..264d06d5f19 100644 --- a/apps/api-extractor/src/analyzer/AstModule.ts +++ b/apps/api-extractor/src/analyzer/AstModule.ts @@ -4,7 +4,7 @@ import * as ts from 'typescript'; import { AstSymbol } from './AstSymbol'; -import { AstEntity } from './AstSymbolTable'; +import { AstEntity } from './AstEntity'; /** * Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo} @@ -16,6 +16,12 @@ export class AstModuleExportInfo { /** * Constructor parameters for AstModule + * + * @privateRemarks + * Our naming convention is to use I____Parameters for constructor options and + * I____Options for general function options. However the word "parameters" is + * confusingly similar to the terminology for function parameters modeled by API Extractor, + * so we use I____Options for both cases in this code base. */ export interface IAstModuleOptions { sourceFile: ts.SourceFile; @@ -25,12 +31,6 @@ export interface IAstModuleOptions { /** * An internal data structure that represents a source file that is analyzed by AstSymbolTable. - * - * @privateRemarks - * Our naming convention is to use I____Parameters for constructor options and - * I____Options for general function options. However the word "parameters" is - * confusingly similar to the terminology for function parameters modeled by API Extractor, - * so we use I____Options for both cases in this code base. */ export class AstModule { /** diff --git a/apps/api-extractor/src/analyzer/AstNamespaceImport.ts b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts new file mode 100644 index 00000000000..955ef903d27 --- /dev/null +++ b/apps/api-extractor/src/analyzer/AstNamespaceImport.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; + +import { AstModule, AstModuleExportInfo } from './AstModule'; +import { AstSyntheticEntity } from './AstEntity'; +import { Collector } from '../collector/Collector'; + +export interface IAstNamespaceImportOptions { + readonly astModule: AstModule; + readonly namespaceName: string; + readonly declaration: ts.Declaration; +} + +/** + * `AstNamespaceImport` represents a namespace that is created implicitly by a statement + * such as `import * as example from "./file";` + * + * @remarks + * + * A typical input looks like this: + * ```ts + * // Suppose that example.ts exports two functions f1() and f2(). + * import * as example from "./file"; + * export { example }; + * ``` + * + * API Extractor's .d.ts rollup will transform it into an explicit namespace, like this: + * ```ts + * declare f1(): void; + * declare f2(): void; + * + * declare namespace example { + * export { + * f1, + * f2 + * } + * } + * ``` + * + * The current implementation does not attempt to relocate f1()/f2() to be inside the `namespace` + * because other type signatures may reference them directly (without using the namespace qualifier). + * The `declare namespace example` is a synthetic construct represented by `AstNamespaceImport`. + */ +export class AstNamespaceImport extends AstSyntheticEntity { + /** + * Returns true if the AstSymbolTable.analyze() was called for this object. + * See that function for details. + */ + public analyzed: boolean = false; + + /** + * For example, if the original statement was `import * as example from "./file";` + * then `astModule` refers to the `./file.d.ts` file. + */ + public readonly astModule: AstModule; + + /** + * For example, if the original statement was `import * as example from "./file";` + * then `namespaceName` would be `example`. + */ + public readonly namespaceName: string; + + /** + * The original `ts.SyntaxKind.NamespaceImport` which can be used as a location for error messages. + */ + public readonly declaration: ts.Declaration; + + public constructor(options: IAstNamespaceImportOptions) { + super(); + this.astModule = options.astModule; + this.namespaceName = options.namespaceName; + this.declaration = options.declaration; + } + + /** {@inheritdoc} */ + public get localName(): string { + // abstract + return this.namespaceName; + } + + public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo { + const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo( + this.astModule + ); + return astModuleExportInfo; + } +} diff --git a/apps/api-extractor/src/analyzer/AstReferenceResolver.ts b/apps/api-extractor/src/analyzer/AstReferenceResolver.ts index 33baaaf2471..6d91f6c194f 100644 --- a/apps/api-extractor/src/analyzer/AstReferenceResolver.ts +++ b/apps/api-extractor/src/analyzer/AstReferenceResolver.ts @@ -4,13 +4,14 @@ import * as ts from 'typescript'; import * as tsdoc from '@microsoft/tsdoc'; -import { AstSymbolTable, AstEntity } from './AstSymbolTable'; +import { AstSymbolTable } from './AstSymbolTable'; +import { AstEntity } from './AstEntity'; import { AstDeclaration } from './AstDeclaration'; import { WorkingPackage } from '../collector/WorkingPackage'; import { AstModule } from './AstModule'; -import { AstImport } from './AstImport'; import { Collector } from '../collector/Collector'; import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import { AstSymbol } from './AstSymbol'; /** * Used by `AstReferenceResolver` to report a failed resolution. @@ -90,8 +91,8 @@ export class AstReferenceResolver { ); } - if (rootAstEntity instanceof AstImport) { - return new ResolverFailure('Reexported declarations are not supported'); + if (!(rootAstEntity instanceof AstSymbol)) { + return new ResolverFailure('This type of declaration is not supported yet by the resolver'); } let currentDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration( diff --git a/apps/api-extractor/src/analyzer/AstSymbol.ts b/apps/api-extractor/src/analyzer/AstSymbol.ts index ce9d2861c1d..fcb3269cf36 100644 --- a/apps/api-extractor/src/analyzer/AstSymbol.ts +++ b/apps/api-extractor/src/analyzer/AstSymbol.ts @@ -4,6 +4,7 @@ import * as ts from 'typescript'; import { AstDeclaration } from './AstDeclaration'; import { InternalError } from '@rushstack/node-core-library'; +import { AstEntity } from './AstEntity'; /** * Constructor options for AstSymbol @@ -50,18 +51,9 @@ export interface IAstSymbolOptions { * - g * ``` */ -export class AstSymbol { - /** - * The original name of the symbol, as exported from the module (i.e. source file) - * containing the original TypeScript definition. Constructs such as - * `import { X as Y } from` may introduce other names that differ from the local name. - * - * @remarks - * For the most part, `localName` corresponds to `followedSymbol.name`, but there - * are some edge cases. For example, the symbol name for `export default class X { }` - * is actually `"default"`, not `"X"`. - */ - public readonly localName: string; +export class AstSymbol extends AstEntity { + /** {@inheritdoc} */ + public readonly localName: string; // abstract /** * If true, then the `followedSymbol` (i.e. original declaration) of this symbol @@ -121,6 +113,8 @@ export class AstSymbol { private _analyzed: boolean = false; public constructor(options: IAstSymbolOptions) { + super(); + this.followedSymbol = options.followedSymbol; this.localName = options.localName; this.isExternal = options.isExternal; diff --git a/apps/api-extractor/src/analyzer/AstSymbolTable.ts b/apps/api-extractor/src/analyzer/AstSymbolTable.ts index 8316161a1b7..e5eae1ffd8c 100644 --- a/apps/api-extractor/src/analyzer/AstSymbolTable.ts +++ b/apps/api-extractor/src/analyzer/AstSymbolTable.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +/* eslint-disable no-bitwise */ // for ts.SymbolFlags + import * as ts from 'typescript'; import { PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; @@ -10,14 +12,13 @@ import { AstSymbol } from './AstSymbol'; import { AstModule, AstModuleExportInfo } from './AstModule'; import { PackageMetadataManager } from './PackageMetadataManager'; import { ExportAnalyzer } from './ExportAnalyzer'; -import { AstImport } from './AstImport'; +import { AstEntity } from './AstEntity'; +import { AstNamespaceImport } from './AstNamespaceImport'; import { MessageRouter } from '../collector/MessageRouter'; import { TypeScriptInternals, IGlobalVariableAnalyzer } from './TypeScriptInternals'; import { StringChecks } from './StringChecks'; import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; -export type AstEntity = AstSymbol | AstImport; - /** * Options for `AstSymbolTable._fetchAstSymbol()` */ @@ -148,41 +149,13 @@ export class AstSymbolTable { * or members. (We do always construct its parents however, since AstDefinition.parent * is immutable, and needed e.g. to calculate release tag inheritance.) */ - public analyze(astSymbol: AstSymbol): void { - if (astSymbol.analyzed) { - return; + public analyze(astEntity: AstEntity): void { + if (astEntity instanceof AstSymbol) { + return this._analyzeAstSymbol(astEntity); } - if (astSymbol.nominalAnalysis) { - // We don't analyze nominal symbols - astSymbol._notifyAnalyzed(); - return; - } - - // Start at the root of the tree - const rootAstSymbol: AstSymbol = astSymbol.rootAstSymbol; - - // Calculate the full child tree for each definition - for (const astDeclaration of rootAstSymbol.astDeclarations) { - this._analyzeChildTree(astDeclaration.declaration, astDeclaration); - } - - rootAstSymbol._notifyAnalyzed(); - - if (!astSymbol.isExternal) { - // 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.analyze(referencedAstEntity); - } - } - } - }); + if (astEntity instanceof AstNamespaceImport) { + return this._analyzeAstNamespaceImport(astEntity); } } @@ -303,6 +276,67 @@ export class AstSymbolTable { return unquotedName; } + private _analyzeAstNamespaceImport(astNamespaceImport: AstNamespaceImport): void { + if (astNamespaceImport.analyzed) { + return; + } + + // mark before actual analyzing, to handle module cyclic reexport + astNamespaceImport.analyzed = true; + + const exportedLocalEntities: Map = this.fetchAstModuleExportInfo( + astNamespaceImport.astModule + ).exportedLocalEntities; + + for (const exportedEntity of exportedLocalEntities.values()) { + this.analyze(exportedEntity); + } + } + + private _analyzeAstSymbol(astSymbol: AstSymbol): void { + if (astSymbol.analyzed) { + return; + } + + if (astSymbol.nominalAnalysis) { + // We don't analyze nominal symbols + astSymbol._notifyAnalyzed(); + return; + } + + // Start at the root of the tree + const rootAstSymbol: AstSymbol = astSymbol.rootAstSymbol; + + // Calculate the full child tree for each definition + for (const astDeclaration of rootAstSymbol.astDeclarations) { + this._analyzeChildTree(astDeclaration.declaration, astDeclaration); + } + + rootAstSymbol._notifyAnalyzed(); + + if (!astSymbol.isExternal) { + // 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); + } + } + + if (referencedAstEntity instanceof AstNamespaceImport) { + if (!referencedAstEntity.astModule.isExternal) { + this._analyzeAstNamespaceImport(referencedAstEntity); + } + } + } + }); + } + } + /** * Used by analyze to recursively analyze the entire child tree. */ @@ -487,13 +521,12 @@ export class AstSymbolTable { } if ( - // eslint-disable-next-line no-bitwise followedSymbol.flags & - // eslint-disable-next-line no-bitwise - (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient) && - !TypeScriptInternals.isLateBoundSymbol(followedSymbol) + (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient) ) { - return undefined; + if (!TypeScriptInternals.isLateBoundSymbol(followedSymbol)) { + return undefined; + } } // API Extractor doesn't analyze ambient declarations at all diff --git a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts index 300fffa155f..46937c8f42f 100644 --- a/apps/api-extractor/src/analyzer/ExportAnalyzer.ts +++ b/apps/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -10,7 +10,9 @@ import { AstImport, IAstImportOptions, AstImportKind } from './AstImport'; import { AstModule, AstModuleExportInfo } from './AstModule'; import { TypeScriptInternals } from './TypeScriptInternals'; import { SourceFileLocationFormatter } from './SourceFileLocationFormatter'; -import { IFetchAstSymbolOptions, AstEntity } from './AstSymbolTable'; +import { IFetchAstSymbolOptions } from './AstSymbolTable'; +import { AstEntity } from './AstEntity'; +import { AstNamespaceImport } from './AstNamespaceImport'; /** * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. @@ -21,7 +23,7 @@ import { IFetchAstSymbolOptions, AstEntity } from './AstSymbolTable'; export interface IAstSymbolTable { fetchAstSymbol(options: IFetchAstSymbolOptions): AstSymbol | undefined; - analyze(astSymbol: AstSymbol): void; + analyze(astEntity: AstEntity): void; } /** @@ -72,6 +74,7 @@ export class ExportAnalyzer { private readonly _importableAmbientSourceFiles: Set = new Set(); private readonly _astImportsByKey: Map = new Map(); + private readonly _astNamespaceImportByModule: Map = new Map(); public constructor( program: ts.Program, @@ -327,6 +330,10 @@ export class ExportAnalyzer { this._astSymbolTable.analyze(astEntity); } + if (astEntity instanceof AstNamespaceImport && !astEntity.astModule.isExternal) { + this._astSymbolTable.analyze(astEntity); + } + astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity); } } @@ -438,6 +445,26 @@ export class ExportAnalyzer { // Example: " ExportName as RenamedName" const exportSpecifier: ts.ExportSpecifier = declaration as ts.ExportSpecifier; exportName = (exportSpecifier.propertyName || exportSpecifier.name).getText().trim(); + } else if (declaration.kind === ts.SyntaxKind.NamespaceExport) { + // EXAMPLE: + // "export * as theLib from 'the-lib';" + // + // ExportDeclaration: + // ExportKeyword: pre=[export] sep=[ ] + // NamespaceExport: + // AsteriskToken: pre=[*] sep=[ ] + // AsKeyword: pre=[as] sep=[ ] + // Identifier: pre=[theLib] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + // Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780 + throw new Error( + `The "export * as ___" syntax is not supported yet; as a workaround,` + + ` use "import * as ___" with a separate "export { ___ }" declaration\n` + + SourceFileLocationFormatter.formatDeclaration(declaration) + ); } else { throw new InternalError( `Unimplemented export declaration kind: ${declaration.getText()}\n` + @@ -497,12 +524,18 @@ export class ExportAnalyzer { // SemicolonToken: pre=[;] if (externalModulePath === undefined) { - // The implementation here only works when importing from an external module. - // The full solution is tracked by: https://github.com/microsoft/rushstack/issues/1029 - throw new Error( - '"import * as ___ from ___;" is not supported yet for local files.\n' + - SourceFileLocationFormatter.formatDeclaration(importDeclaration) - ); + const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol); + let namespaceImport: AstNamespaceImport | undefined = + this._astNamespaceImportByModule.get(astModule); + if (namespaceImport === undefined) { + namespaceImport = new AstNamespaceImport({ + namespaceName: declarationSymbol.name, + astModule: astModule, + declaration: declaration + }); + this._astNamespaceImportByModule.set(astModule, namespaceImport); + } + return namespaceImport; } // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 7f54fd4afb7..c963a448fe4 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -9,7 +9,8 @@ import { ReleaseTag } from '@microsoft/api-extractor-model'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; import { CollectorEntity } from './CollectorEntity'; -import { AstSymbolTable, AstEntity } from '../analyzer/AstSymbolTable'; +import { AstSymbolTable } from '../analyzer/AstSymbolTable'; +import { AstEntity } from '../analyzer/AstEntity'; import { AstModule, AstModuleExportInfo } from '../analyzer/AstModule'; import { AstSymbol } from '../analyzer/AstSymbol'; import { AstDeclaration } from '../analyzer/AstDeclaration'; @@ -23,6 +24,8 @@ import { TypeScriptInternals, IGlobalVariableAnalyzer } from '../analyzer/TypeSc import { MessageRouter } from './MessageRouter'; import { AstReferenceResolver } from '../analyzer/AstReferenceResolver'; import { ExtractorConfig } from '../api/ExtractorConfig'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; +import { AstImport } from '../analyzer/AstImport'; /** * Options for Collector constructor. @@ -231,6 +234,7 @@ export class Collector { const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint); + for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) { this._createCollectorEntity(astEntity, exportName); @@ -311,9 +315,10 @@ export class Collector { if (astEntity instanceof AstSymbol) { return this.fetchSymbolMetadata(astEntity); } - if (astEntity.astSymbol) { - // astImport - return this.fetchSymbolMetadata(astEntity.astSymbol); + if (astEntity instanceof AstImport) { + if (astEntity.astSymbol) { + return this.fetchSymbolMetadata(astEntity.astSymbol); + } } return undefined; } @@ -389,7 +394,7 @@ export class Collector { return overloadIndex; } - private _createCollectorEntity(astEntity: AstEntity, exportedName: string | undefined): void { + private _createCollectorEntity(astEntity: AstEntity, exportedName: string | undefined): CollectorEntity { let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity); if (!entity) { @@ -397,15 +402,14 @@ export class Collector { this._entitiesByAstEntity.set(astEntity, entity); this._entities.push(entity); - - if (astEntity instanceof AstSymbol) { - this._collectReferenceDirectives(astEntity); - } + this._collectReferenceDirectives(astEntity); } if (exportedName) { entity.addExportName(exportedName); } + + return entity; } private _createEntityForIndirectReferences( @@ -435,6 +439,18 @@ export class Collector { } }); } + + if (astEntity instanceof AstNamespaceImport) { + const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this); + + for (const exportedEntity of astModuleExportInfo.exportedLocalEntities.values()) { + // Create a CollectorEntity for each top-level export of AstImportInternal entity + const entity: CollectorEntity = this._createCollectorEntity(exportedEntity, undefined); + entity.addAstNamespaceImports(astEntity); + + this._createEntityForIndirectReferences(exportedEntity, alreadySeenAstEntities); + } + } } /** @@ -770,7 +786,7 @@ export class Collector { // Don't report missing release tags for forgotten exports const astSymbol: AstSymbol = astDeclaration.astSymbol; const entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astSymbol.rootAstSymbol); - if (entity && entity.exported) { + if (entity && entity.consumable) { // We also don't report errors for the default export of an entry point, since its doc comment // isn't easy to obtain from the .d.ts file if (astSymbol.rootAstSymbol.localName !== '_default') { @@ -857,11 +873,24 @@ export class Collector { return parserContext; } - private _collectReferenceDirectives(astSymbol: AstSymbol): void { + private _collectReferenceDirectives(astEntity: AstEntity): void { + if (astEntity instanceof AstSymbol) { + const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) => + astDeclaration.declaration.getSourceFile() + ); + return this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + } + + if (astEntity instanceof AstNamespaceImport) { + const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile]; + return this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + } + } + + private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void { const seenFilenames: Set = new Set(); - for (const astDeclaration of astSymbol.astDeclarations) { - const sourceFile: ts.SourceFile = astDeclaration.declaration.getSourceFile(); + for (const sourceFile of sourceFiles) { if (sourceFile && sourceFile.fileName) { if (!seenFilenames.has(sourceFile.fileName)) { seenFilenames.add(sourceFile.fileName); diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index a4e9f3f330b..23f57db2c6b 100644 --- a/apps/api-extractor/src/collector/CollectorEntity.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -6,7 +6,8 @@ import * as ts from 'typescript'; import { AstSymbol } from '../analyzer/AstSymbol'; import { Collector } from './Collector'; import { Sort } from '@rushstack/node-core-library'; -import { AstEntity } from '../analyzer/AstSymbolTable'; +import { AstEntity } from '../analyzer/AstEntity'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; /** * This is a data structure used by the Collector to track an AstEntity that may be emitted in the *.d.ts file. @@ -15,7 +16,7 @@ import { AstEntity } from '../analyzer/AstSymbolTable'; * The additional contextual state beyond AstSymbol is: * - Whether it's an export of this entry point or not * - The nameForEmit, which may get renamed by DtsRollupGenerator._makeUniqueNames() - * - The export name (or names, if the same declaration is exported multiple times) + * - The export name (or names, if the same symbol is exported multiple times) */ export class CollectorEntity { /** @@ -23,7 +24,7 @@ export class CollectorEntity { */ public readonly astEntity: AstEntity; - private _exportNames: Set = new Set(); + private _exportNames: Set = new Set(); private _exportNamesSorted: boolean = false; private _singleExportName: string | undefined = undefined; @@ -31,6 +32,8 @@ export class CollectorEntity { private _sortKey: string | undefined = undefined; + private _astNamespaceImports: Set = new Set(); + public constructor(astEntity: AstEntity) { this.astEntity = astEntity; } @@ -101,6 +104,48 @@ export class CollectorEntity { return this.exportNames.size > 0; } + /** + * Indicates that it is possible for a consumer of the API to access this declaration, either by importing + * it directly, or via some other alias such as a member of a namespace. If a collector entity is not consumable, + * then API Extractor will report a ExtractorMessageId.ForgottenExport warning. + * + * @remarks + * Generally speaking, an API item is consumable if: + * + * - The collector encounters it while crawling the entry point, and it is a root symbol + * (i.e. there is a corresponding a CollectorEntity) + * + * - AND it is exported by the entry point + * + * However a special case occurs with `AstNamespaceImport` which produces a rollup like this: + * + * ```ts + * declare interface IForgottenExport { } + * + * declare function member(): IForgottenExport; + * + * declare namespace ns { + * export { + * member + * } + * } + * export { ns } + * ``` + * + * In this example, `IForgottenExport` is not consumable. Whereas `member()` is consumable as `ns.member()` + * even though `member()` itself is not exported. + */ + public get consumable(): boolean { + return this.exported || this._astNamespaceImports.size > 0; + } + + /** + * Associates this entity with a `AstNamespaceImport`. + */ + public addAstNamespaceImports(astNamespaceImport: AstNamespaceImport): void { + this._astNamespaceImports.add(astNamespaceImport); + } + /** * Adds a new exportName to the exportNames set. */ diff --git a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts index b3596062b57..ee7637d0161 100644 --- a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts +++ b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts @@ -28,7 +28,7 @@ export class DocCommentEnhancer { public analyze(): void { for (const entity of this._collector.entities) { if (entity.astEntity instanceof AstSymbol) { - if (entity.exported) { + if (entity.consumable) { entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { this._analyzeApiItem(astDeclaration); }); diff --git a/apps/api-extractor/src/enhancers/ValidationEnhancer.ts b/apps/api-extractor/src/enhancers/ValidationEnhancer.ts index 5e6ed43b75e..302480ee86d 100644 --- a/apps/api-extractor/src/enhancers/ValidationEnhancer.ts +++ b/apps/api-extractor/src/enhancers/ValidationEnhancer.ts @@ -12,21 +12,52 @@ import { SymbolMetadata } from '../collector/SymbolMetadata'; import { CollectorEntity } from '../collector/CollectorEntity'; import { ExtractorMessageId } from '../api/ExtractorMessageId'; import { ReleaseTag } from '@microsoft/api-extractor-model'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; +import { AstModuleExportInfo } from '../analyzer/AstModule'; +import { AstEntity } from '../analyzer/AstEntity'; export class ValidationEnhancer { public static analyze(collector: Collector): void { - const alreadyWarnedSymbols: Set = new Set(); + const alreadyWarnedEntities: Set = new Set(); for (const entity of collector.entities) { + if (!entity.consumable) { + continue; + } + if (entity.astEntity instanceof AstSymbol) { - if (entity.exported) { - entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { - ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedSymbols); - }); - - const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(entity.astEntity); - ValidationEnhancer._checkForInternalUnderscore(collector, entity, entity.astEntity, symbolMetadata); - ValidationEnhancer._checkForInconsistentReleaseTags(collector, entity.astEntity, symbolMetadata); + // A regular exported AstSymbol + + const astSymbol: AstSymbol = entity.astEntity; + + astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities); + }); + + const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol); + ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata); + ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata); + } else if (entity.astEntity instanceof AstNamespaceImport) { + // A namespace created using "import * as ___ from ___" + const astNamespaceImport: AstNamespaceImport = entity.astEntity; + + const astModuleExportInfo: AstModuleExportInfo = + astNamespaceImport.fetchAstModuleExportInfo(collector); + + for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) { + if (namespaceMemberAstEntity instanceof AstSymbol) { + const astSymbol: AstSymbol = namespaceMemberAstEntity; + + astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities); + }); + + const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol); + + // (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members) + + ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata); + } } } } @@ -158,12 +189,16 @@ export class ValidationEnhancer { private static _checkReferences( collector: Collector, astDeclaration: AstDeclaration, - alreadyWarnedSymbols: Set + alreadyWarnedEntities: Set ): void { const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); const declarationReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; for (const referencedEntity of astDeclaration.referencedAstEntities) { + let collectorEntity: CollectorEntity | undefined; + let referencedReleaseTag: ReleaseTag; + let localName: string; + if (referencedEntity instanceof AstSymbol) { // If this is e.g. a member of a namespace, then we need to be checking the top-level scope to see // whether it's exported. @@ -171,42 +206,58 @@ export class ValidationEnhancer { // TODO: Technically we should also check each of the nested scopes along the way. const rootSymbol: AstSymbol = referencedEntity.rootAstSymbol; - if (!rootSymbol.isExternal) { - const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(rootSymbol); - - if (collectorEntity && collectorEntity.exported) { - const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity); - const referencedReleaseTag: ReleaseTag = referencedMetadata.maxEffectiveReleaseTag; - - if (ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) { - collector.messageRouter.addAnalyzerIssue( - ExtractorMessageId.IncompatibleReleaseTags, - `The symbol "${astDeclaration.astSymbol.localName}"` + - ` is marked as ${ReleaseTag.getTagName(declarationReleaseTag)},` + - ` but its signature references "${referencedEntity.localName}"` + - ` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`, - astDeclaration - ); - } + if (rootSymbol.isExternal) { + continue; + } + + localName = rootSymbol.localName; + + collectorEntity = collector.tryGetCollectorEntity(rootSymbol); + + const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity); + referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag; + } else if (referencedEntity instanceof AstNamespaceImport) { + collectorEntity = collector.tryGetCollectorEntity(referencedEntity); + + // TODO: Currently the "import * as ___ from ___" syntax does not yet support doc comments + referencedReleaseTag = ReleaseTag.Public; + + localName = referencedEntity.localName; + } else { + continue; + } + + if (collectorEntity && collectorEntity.consumable) { + if (ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.IncompatibleReleaseTags, + `The symbol "${astDeclaration.astSymbol.localName}"` + + ` is marked as ${ReleaseTag.getTagName(declarationReleaseTag)},` + + ` but its signature references "${referencedEntity.localName}"` + + ` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`, + astDeclaration + ); + } + } else { + const entryPointFilename: string = path.basename( + collector.workingPackage.entryPointSourceFile.fileName + ); + + if (!alreadyWarnedEntities.has(referencedEntity)) { + alreadyWarnedEntities.add(referencedEntity); + + if ( + referencedEntity instanceof AstSymbol && + ValidationEnhancer._isEcmaScriptSymbol(referencedEntity) + ) { + // The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object, + // so as a special case, we do NOT report them as forgotten exports. } else { - const entryPointFilename: string = path.basename( - collector.workingPackage.entryPointSourceFile.fileName + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.ForgottenExport, + `The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`, + astDeclaration ); - - if (!alreadyWarnedSymbols.has(referencedEntity)) { - alreadyWarnedSymbols.add(referencedEntity); - - // The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object, - // so as a special case, we do NOT report them as forgotten exports. - if (!ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) { - collector.messageRouter.addAnalyzerIssue( - ExtractorMessageId.ForgottenExport, - `The symbol "${rootSymbol.localName}" needs to be exported` + - ` by the entry point ${entryPointFilename}`, - astDeclaration - ); - } - } } } } diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts index ade21455630..648a762a0b6 100644 --- a/apps/api-extractor/src/generators/ApiModelGenerator.ts +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -40,6 +40,9 @@ import { AstSymbol } from '../analyzer/AstSymbol'; import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator'; import { ApiItemMetadata } from '../collector/ApiItemMetadata'; import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; +import { AstEntity } from '../analyzer/AstEntity'; +import { AstModule } from '../analyzer/AstModule'; export class ApiModelGenerator { private readonly _collector: Collector; @@ -78,22 +81,79 @@ export class ApiModelGenerator { // Create a CollectorEntity for each top-level export for (const entity of this._collector.entities) { if (entity.exported) { - if (entity.astEntity instanceof AstSymbol) { - // Skip ancillary declarations; we will process them with the main declaration - for (const astDeclaration of this._collector.getNonAncillaryDeclarations(entity.astEntity)) { - this._processDeclaration(astDeclaration, entity.nameForEmit, apiEntryPoint); - } - } else { - // TODO: Figure out how to represent reexported AstImport objects. Basically we need to introduce a new - // ApiItem subclass for "export alias", similar to a type alias, but representing declarations of the - // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. - } + this._processAstEntity(entity.astEntity, entity.nameForEmit, apiEntryPoint); } } return apiPackage; } + private _processAstEntity( + astEntity: AstEntity, + exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin + ): void { + if (astEntity instanceof AstSymbol) { + // Skip ancillary declarations; we will process them with the main declaration + for (const astDeclaration of this._collector.getNonAncillaryDeclarations(astEntity)) { + this._processDeclaration(astDeclaration, exportedName, parentApiItem); + } + return; + } + + if (astEntity instanceof AstNamespaceImport) { + // Note that a single API item can belong to two different AstNamespaceImport namespaces. For example: + // + // // file.ts defines "thing()" + // import * as example1 from "./file"; + // import * as example2 from "./file"; + // + // // ...so here we end up with example1.thing() and example2.thing() + // export { example1, example2 } + // + // The current logic does not try to associate "thing()" with a specific parent. Instead + // the API documentation will show duplicated entries for example1.thing() and example2.thing()./ + // + // This could be improved in the future, but it requires a stable mechanism for choosing an associated parent. + // For thoughts about this: https://github.com/microsoft/rushstack/issues/1308 + this._processAstModule(astEntity.astModule, exportedName, parentApiItem); + return; + } + + // TODO: Figure out how to represent reexported AstImport objects. Basically we need to introduce a new + // ApiItem subclass for "export alias", similar to a type alias, but representing declarations of the + // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. + } + + private _processAstModule( + astModule: AstModule, + exportedName: string | undefined, + parentApiItem: ApiItemContainerMixin + ): void { + const name: string = exportedName ? exportedName : astModule.moduleSymbol.name; + const containerKey: string = ApiNamespace.getContainerKey(name); + + let apiNamespace: ApiNamespace | undefined = parentApiItem.tryGetMemberByKey( + containerKey + ) as ApiNamespace; + + if (apiNamespace === undefined) { + apiNamespace = new ApiNamespace({ + name, + docComment: undefined, + releaseTag: ReleaseTag.None, + excerptTokens: [] + }); + parentApiItem.addMember(apiNamespace); + } + + astModule.astModuleExportInfo!.exportedLocalEntities.forEach( + (exportedEntity: AstEntity, exportedName: string) => { + this._processAstEntity(exportedEntity, exportedName, apiNamespace!); + } + ); + } + private _processDeclaration( astDeclaration: AstDeclaration, exportedName: string | undefined, diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index fc494c0dd16..e9a40e283e0 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -16,6 +16,10 @@ import { AstSymbol } from '../analyzer/AstSymbol'; import { ExtractorMessage } from '../api/ExtractorMessage'; import { StringWriter } from './StringWriter'; import { DtsEmitHelpers } from './DtsEmitHelpers'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; +import { AstEntity } from '../analyzer/AstEntity'; +import { AstModuleExportInfo } from '../analyzer/AstModule'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; export class ApiReportGenerator { private static _trimSpacesRegExp: RegExp = / +$/gm; @@ -52,6 +56,22 @@ export class ApiReportGenerator { // Write the opening delimiter for the Markdown code fence stringWriter.writeLine('```ts\n'); + // Emit the triple slash directives + let directivesEmitted: boolean = false; + for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) { + // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 + stringWriter.writeLine(`/// `); + directivesEmitted = true; + } + + for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) { + stringWriter.writeLine(`/// `); + directivesEmitted = true; + } + if (directivesEmitted) { + stringWriter.writeLine(); + } + // Emit the imports let importsEmitted: boolean = false; for (const entity of collector.entities) { @@ -60,14 +80,14 @@ export class ApiReportGenerator { importsEmitted = true; } } - if (importsEmitted) { stringWriter.writeLine(); } // Emit the regular declarations for (const entity of collector.entities) { - if (entity.exported) { + const astEntity: AstEntity = entity.astEntity; + if (entity.consumable) { // First, collect the list of export names for this symbol. When reporting messages with // ExtractorMessage.properties.exportName, this will enable us to emit the warning comments alongside // the associated export statement. @@ -83,9 +103,9 @@ export class ApiReportGenerator { } } - if (entity.astEntity instanceof AstSymbol) { + if (astEntity instanceof AstSymbol) { // Emit all the declarations for this entity - for (const astDeclaration of entity.astEntity.astDeclarations || []) { + for (const astDeclaration of astEntity.astDeclarations || []) { // Get the messages associated with this declaration const fetchedMessages: ExtractorMessage[] = collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration); @@ -125,6 +145,63 @@ export class ApiReportGenerator { } } + if (astEntity instanceof AstNamespaceImport) { + const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); + + if (entity.nameForEmit === undefined) { + // This should never happen + throw new InternalError('referencedEntry.nameForEmit is undefined'); + } + + if (astModuleExportInfo.starExportedExternalModules.size > 0) { + // We could support this, but we would need to find a way to safely represent it. + throw new Error( + `The ${entity.nameForEmit} namespace import includes a start export, which is not supported:\n` + + SourceFileLocationFormatter.formatDeclaration(astEntity.declaration) + ); + } + + // Emit a synthetic declaration for the namespace. It will look like this: + // + // declare namespace example { + // export { + // f1, + // f2 + // } + // } + // + // Note that we do not try to relocate f1()/f2() to be inside the namespace because other type + // signatures may reference them directly (without using the namespace qualifier). + + stringWriter.writeLine(`declare namespace ${entity.nameForEmit} {`); + + // all local exports of local imported module are just references to top-level declarations + stringWriter.writeLine(' export {'); + + const exportClauses: string[] = []; + for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) { + const collectorEntity: CollectorEntity | undefined = + collector.tryGetCollectorEntity(exportedEntity); + if (collectorEntity === undefined) { + // This should never happen + // top-level exports of local imported module should be added as collector entities before + throw new InternalError( + `Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}` + ); + } + + if (collectorEntity.nameForEmit === exportedName) { + exportClauses.push(collectorEntity.nameForEmit); + } else { + exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`); + } + } + stringWriter.writeLine(exportClauses.map((x) => ` ${x}`).join(',\n')); + + stringWriter.writeLine(' }'); // end of "export { ... }" + stringWriter.writeLine('}'); // end of "declare namespace { ... }" + } + // Now emit the export statements for this entity. for (const exportToEmit of exportsToEmit.values()) { // Write any associated messages diff --git a/apps/api-extractor/src/generators/DtsRollupGenerator.ts b/apps/api-extractor/src/generators/DtsRollupGenerator.ts index e0d7245c469..452dcd3b37e 100644 --- a/apps/api-extractor/src/generators/DtsRollupGenerator.ts +++ b/apps/api-extractor/src/generators/DtsRollupGenerator.ts @@ -19,6 +19,10 @@ import { SymbolMetadata } from '../collector/SymbolMetadata'; import { StringWriter } from './StringWriter'; import { DtsEmitHelpers } from './DtsEmitHelpers'; import { DeclarationMetadata } from '../collector/DeclarationMetadata'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; +import { AstModuleExportInfo } from '../analyzer/AstModule'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter'; +import { AstEntity } from '../analyzer/AstEntity'; /** * Used with DtsRollupGenerator.writeTypingsFile() @@ -72,19 +76,26 @@ export class DtsRollupGenerator { stringWriter: StringWriter, dtsKind: DtsRollupKind ): void { + // Emit the @packageDocumentation comment at the top of the file if (collector.workingPackage.tsdocParserContext) { stringWriter.writeLine(collector.workingPackage.tsdocParserContext.sourceRange.toString()); stringWriter.writeLine(); } // Emit the triple slash directives + let directivesEmitted: boolean = false; for (const typeDirectiveReference of collector.dtsTypeReferenceDirectives) { // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 stringWriter.writeLine(`/// `); + directivesEmitted = true; } for (const libDirectiveReference of collector.dtsLibReferenceDirectives) { stringWriter.writeLine(`/// `); + directivesEmitted = true; + } + if (directivesEmitted) { + stringWriter.writeLine(); } // Emit the imports @@ -107,9 +118,8 @@ export class DtsRollupGenerator { // Emit the regular declarations for (const entity of collector.entities) { - const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity( - entity.astEntity - ); + const astEntity: AstEntity = entity.astEntity; + const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity); const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata ? symbolMetadata.maxEffectiveReleaseTag : ReleaseTag.None; @@ -122,9 +132,9 @@ export class DtsRollupGenerator { continue; } - if (entity.astEntity instanceof AstSymbol) { + if (astEntity instanceof AstSymbol) { // Emit all the declarations for this entry - for (const astDeclaration of entity.astEntity.astDeclarations || []) { + for (const astDeclaration of astEntity.astDeclarations || []) { const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); if (!this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, dtsKind)) { @@ -144,6 +154,67 @@ export class DtsRollupGenerator { } } + if (astEntity instanceof AstNamespaceImport) { + const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); + + if (entity.nameForEmit === undefined) { + // This should never happen + throw new InternalError('referencedEntry.nameForEmit is undefined'); + } + + if (astModuleExportInfo.starExportedExternalModules.size > 0) { + // We could support this, but we would need to find a way to safely represent it. + throw new Error( + `The ${entity.nameForEmit} namespace import includes a start export, which is not supported:\n` + + SourceFileLocationFormatter.formatDeclaration(astEntity.declaration) + ); + } + + // Emit a synthetic declaration for the namespace. It will look like this: + // + // declare namespace example { + // export { + // f1, + // f2 + // } + // } + // + // Note that we do not try to relocate f1()/f2() to be inside the namespace because other type + // signatures may reference them directly (without using the namespace qualifier). + + stringWriter.writeLine(); + if (entity.shouldInlineExport) { + stringWriter.write('export '); + } + stringWriter.writeLine(`declare namespace ${entity.nameForEmit} {`); + + // all local exports of local imported module are just references to top-level declarations + stringWriter.writeLine(' export {'); + + const exportClauses: string[] = []; + for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) { + const collectorEntity: CollectorEntity | undefined = + collector.tryGetCollectorEntity(exportedEntity); + if (collectorEntity === undefined) { + // This should never happen + // top-level exports of local imported module should be added as collector entities before + throw new InternalError( + `Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}` + ); + } + + if (collectorEntity.nameForEmit === exportedName) { + exportClauses.push(collectorEntity.nameForEmit); + } else { + exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`); + } + } + stringWriter.writeLine(exportClauses.map((x) => ` ${x}`).join(',\n')); + + stringWriter.writeLine(' }'); // end of "export { ... }" + stringWriter.writeLine('}'); // end of "declare namespace { ... }" + } + if (!entity.shouldInlineExport) { for (const exportName of entity.exportNames) { DtsEmitHelpers.emitNamedExport(stringWriter, exportName, entity); diff --git a/build-tests/api-extractor-scenarios/config/build-config.json b/build-tests/api-extractor-scenarios/config/build-config.json index 80ec5180ec8..b2716da13c5 100644 --- a/build-tests/api-extractor-scenarios/config/build-config.json +++ b/build-tests/api-extractor-scenarios/config/build-config.json @@ -20,6 +20,8 @@ "exportImportedExternal", "exportImportedExternal2", "exportImportedExternalDefault", + "exportImportStarAs", + "exportImportStarAs2", "exportStar", "exportStar2", "exportStar3", diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..e2d42c40dbc --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.json @@ -0,0 +1,469 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1004, + "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": "@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 + } + } + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "members": [ + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!calculator:namespace", + "docComment": "", + "excerptTokens": [], + "releaseTag": "None", + "name": "calculator", + "members": [ + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!calculator.add:function(1)", + "docComment": "/**\n * Returns the sum of adding `b` to `a`\n *\n * @param a - first number\n *\n * @param b - second number\n *\n * @returns Sum of adding `b` to `a`\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function add(a: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ", b: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "a", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "parameterName": "b", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + } + } + ], + "name": "add" + }, + { + "kind": "Variable", + "canonicalReference": "api-extractor-scenarios!calculator.calucatorVersion:var", + "docComment": "/**\n * Returns the version of the calculator.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "calucatorVersion: " + }, + { + "kind": "Content", + "text": "string" + } + ], + "releaseTag": "Public", + "name": "calucatorVersion", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!calculator.subtract:function(1)", + "docComment": "/**\n * Returns the sum of subtracting `b` from `a`\n *\n * @param a - first number\n *\n * @param b - second number\n *\n * @returns Sum of subtract `b` from `a`\n *\n * @beta\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function subtract(a: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ", b: " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Beta", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "a", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "parameterName": "b", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + } + } + ], + "name": "subtract" + } + ] + }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!calculator2:namespace", + "docComment": "", + "excerptTokens": [], + "releaseTag": "None", + "name": "calculator2", + "members": [ + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!calculator2.add:function(1)", + "docComment": "/**\n * Returns the sum of adding `b` to `a` for large integers\n *\n * @param a - first number\n *\n * @param b - second number\n *\n * @returns Sum of adding `b` to `a`\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function add(a: " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": ", b: " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "a", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "parameterName": "b", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + } + } + ], + "name": "add" + }, + { + "kind": "Variable", + "canonicalReference": "api-extractor-scenarios!calculator2.calucatorVersion:var", + "docComment": "/**\n * Returns the version of the calculator.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "calucatorVersion: " + }, + { + "kind": "Content", + "text": "string" + } + ], + "releaseTag": "Public", + "name": "calucatorVersion", + "variableTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!calculator2.subtract:function(1)", + "docComment": "/**\n * Returns the sum of subtracting `b` from `a` for large integers\n *\n * @param a - first number\n *\n * @param b - second number\n *\n * @returns Sum of subtract `b` from `a`\n *\n * @beta\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function subtract(a: " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": ", b: " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "bigint" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 5, + "endIndex": 6 + }, + "releaseTag": "Beta", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "a", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "parameterName": "b", + "parameterTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + } + } + ], + "name": "subtract" + } + ] + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..3bbae48831e --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/api-extractor-scenarios.api.md @@ -0,0 +1,43 @@ +## 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 +function add(a: number, b: number): number; + +// @public +function add_2(a: bigint, b: bigint): bigint; + +declare namespace calculator { + export { + add, + subtract, + calucatorVersion + } +} +export { calculator } + +declare namespace calculator2 { + export { + add_2 as add, + subtract_2 as subtract, + calucatorVersion + } +} +export { calculator2 } + +// @public +const calucatorVersion: string; + +// @beta +function subtract(a: number, b: number): number; + +// @beta +function subtract_2(a: bigint, b: bigint): bigint; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/rollup.d.ts new file mode 100644 index 00000000000..2739b94b6fc --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs/rollup.d.ts @@ -0,0 +1,62 @@ + +/** + * Returns the sum of adding `b` to `a` + * @param a - first number + * @param b - second number + * @returns Sum of adding `b` to `a` + * @public + */ +declare function add(a: number, b: number): number; + +/** + * Returns the sum of adding `b` to `a` for large integers + * @param a - first number + * @param b - second number + * @returns Sum of adding `b` to `a` + * @public + */ +declare function add_2(a: bigint, b: bigint): bigint; + +declare namespace calculator { + export { + add, + subtract, + calucatorVersion + } +} +export { calculator } + +declare namespace calculator2 { + export { + add_2 as add, + subtract_2 as subtract, + calucatorVersion + } +} +export { calculator2 } + +/** + * Returns the version of the calculator. + * @public + */ +declare const calucatorVersion: string; + +/** + * Returns the sum of subtracting `b` from `a` + * @param a - first number + * @param b - second number + * @returns Sum of subtract `b` from `a` + * @beta + */ +declare function subtract(a: number, b: number): number; + +/** + * Returns the sum of subtracting `b` from `a` for large integers + * @param a - first number + * @param b - second number + * @returns Sum of subtract `b` from `a` + * @beta + */ +declare function subtract_2(a: bigint, b: bigint): bigint; + +export { } diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..28fe5c46bfe --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.json @@ -0,0 +1,217 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1004, + "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": "@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 + } + } + }, + "kind": "Package", + "canonicalReference": "api-extractor-scenarios!", + "docComment": "", + "name": "api-extractor-scenarios", + "members": [ + { + "kind": "EntryPoint", + "canonicalReference": "api-extractor-scenarios!", + "name": "", + "members": [ + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!ns:namespace", + "docComment": "", + "excerptTokens": [], + "releaseTag": "None", + "name": "ns", + "members": [ + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!ns.exportedApi:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function exportedApi(): " + }, + { + "kind": "Content", + "text": "forgottenNs." + }, + { + "kind": "Reference", + "text": "ForgottenClass", + "canonicalReference": "api-extractor-scenarios!ForgottenClass:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "exportedApi" + } + ] + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..cdb87e341e9 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/api-extractor-scenarios.api.md @@ -0,0 +1,26 @@ +## 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 + +// Warning: (ae-forgotten-export) The symbol "forgottenNs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function exportedApi(): forgottenNs.ForgottenClass; + +// @public (undocumented) +class ForgottenClass { +} + +declare namespace ns { + export { + exportedApi + } +} +export { ns } + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/rollup.d.ts new file mode 100644 index 00000000000..e171807fd0f --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/test-outputs/exportImportStarAs2/rollup.d.ts @@ -0,0 +1,26 @@ + +/** + * @public + */ +declare function exportedApi(): forgottenNs.ForgottenClass; + +/** + * @public + */ +declare class ForgottenClass { +} + +declare namespace forgottenNs { + export { + ForgottenClass + } +} + +declare namespace ns { + export { + exportedApi + } +} +export { ns } + +export { } diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator.ts new file mode 100644 index 00000000000..17df6809b45 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator.ts @@ -0,0 +1,23 @@ +/** + * Returns the sum of adding `b` to `a` + * @param a - first number + * @param b - second number + * @returns Sum of adding `b` to `a` + * @public + */ +export function add(a: number, b: number): number { + return a + b; +} + +/** + * Returns the sum of subtracting `b` from `a` + * @param a - first number + * @param b - second number + * @returns Sum of subtract `b` from `a` + * @beta + */ +export function subtract(a: number, b: number): number { + return a - b; +} + +export * from './common'; diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator2.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator2.ts new file mode 100644 index 00000000000..38b5ef6950a --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs/calculator2.ts @@ -0,0 +1,23 @@ +/** + * Returns the sum of adding `b` to `a` for large integers + * @param a - first number + * @param b - second number + * @returns Sum of adding `b` to `a` + * @public + */ +export function add(a: bigint, b: bigint): bigint { + return a + b; +} + +/** + * Returns the sum of subtracting `b` from `a` for large integers + * @param a - first number + * @param b - second number + * @returns Sum of subtract `b` from `a` + * @beta + */ +export function subtract(a: bigint, b: bigint): bigint { + return a - b; +} + +export * from './common'; diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts new file mode 100644 index 00000000000..1e4914ce34e --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts @@ -0,0 +1,5 @@ +/** + * Returns the version of the calculator. + * @public + */ +export const calucatorVersion: string = '1.0.0'; diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs/index.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs/index.ts new file mode 100644 index 00000000000..f2b54960292 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs/index.ts @@ -0,0 +1,5 @@ +import * as calculator from './calculator'; +export { calculator }; + +import * as calculator2 from './calculator2'; +export { calculator2 }; diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs2/forgottenNs.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/forgottenNs.ts new file mode 100644 index 00000000000..fc104acc837 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/forgottenNs.ts @@ -0,0 +1,4 @@ +/** + * @public + */ +export class ForgottenClass {} diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs2/index.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/index.ts new file mode 100644 index 00000000000..2e3ab2c86ca --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/index.ts @@ -0,0 +1,2 @@ +import * as ns from './ns'; +export { ns }; diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs2/ns.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/ns.ts new file mode 100644 index 00000000000..5ea39d1dc1c --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs2/ns.ts @@ -0,0 +1,8 @@ +import * as forgottenNs from './forgottenNs'; + +/** + * @public + */ +export function exportedApi(): forgottenNs.ForgottenClass { + return new forgottenNs.ForgottenClass(); +} diff --git a/build-tests/api-extractor-test-01/dist/api-extractor-test-01-beta.d.ts b/build-tests/api-extractor-test-01/dist/api-extractor-test-01-beta.d.ts index 5c348e3cd95..bed419876f7 100644 --- a/build-tests/api-extractor-test-01/dist/api-extractor-test-01-beta.d.ts +++ b/build-tests/api-extractor-test-01/dist/api-extractor-test-01-beta.d.ts @@ -12,6 +12,7 @@ /// /// /// + import { default as Long_2 } from 'long'; import { MAX_UNSIGNED_VALUE } from 'long'; diff --git a/build-tests/api-extractor-test-01/dist/api-extractor-test-01-public.d.ts b/build-tests/api-extractor-test-01/dist/api-extractor-test-01-public.d.ts index 4f8d48dcead..3b331dcbbfa 100644 --- a/build-tests/api-extractor-test-01/dist/api-extractor-test-01-public.d.ts +++ b/build-tests/api-extractor-test-01/dist/api-extractor-test-01-public.d.ts @@ -12,6 +12,7 @@ /// /// /// + import { default as Long_2 } from 'long'; import { MAX_UNSIGNED_VALUE } from 'long'; diff --git a/build-tests/api-extractor-test-01/dist/api-extractor-test-01.d.ts b/build-tests/api-extractor-test-01/dist/api-extractor-test-01.d.ts index dfcd640e294..dfc4227c12e 100644 --- a/build-tests/api-extractor-test-01/dist/api-extractor-test-01.d.ts +++ b/build-tests/api-extractor-test-01/dist/api-extractor-test-01.d.ts @@ -12,6 +12,7 @@ /// /// /// + import { default as Long_2 } from 'long'; import { MAX_UNSIGNED_VALUE } from 'long'; diff --git a/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.md b/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.md index fe3cbefe315..87e44be72c0 100644 --- a/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.md +++ b/build-tests/api-extractor-test-01/etc/api-extractor-test-01.api.md @@ -4,6 +4,10 @@ ```ts +/// +/// +/// + import { default as Long_2 } from 'long'; import { MAX_UNSIGNED_VALUE } from 'long'; diff --git a/build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml b/build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml index 37659e3b16a..ccdd377ab35 100644 --- a/build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml +++ b/build-tests/install-test-workspace/workspace/common/pnpm-lock.yaml @@ -5,13 +5,13 @@ importers: typescript-newest-test: specifiers: '@rushstack/eslint-config': file:rushstack-eslint-config-2.3.4.tgz - '@rushstack/heft': file:rushstack-heft-0.33.0.tgz + '@rushstack/heft': file:rushstack-heft-0.34.0.tgz eslint: ~7.12.1 tslint: ~5.20.1 typescript: ~4.3.2 devDependencies: '@rushstack/eslint-config': file:../temp/tarballs/rushstack-eslint-config-2.3.4.tgz_eslint@7.12.1+typescript@4.3.2 - '@rushstack/heft': file:../temp/tarballs/rushstack-heft-0.33.0.tgz + '@rushstack/heft': file:../temp/tarballs/rushstack-heft-0.34.0.tgz eslint: 7.12.1 tslint: 5.20.1_typescript@4.3.2 typescript: 4.3.2 @@ -2771,10 +2771,10 @@ packages: - typescript dev: true - file:../temp/tarballs/rushstack-heft-0.33.0.tgz: - resolution: {tarball: file:../temp/tarballs/rushstack-heft-0.33.0.tgz} + file:../temp/tarballs/rushstack-heft-0.34.0.tgz: + resolution: {tarball: file:../temp/tarballs/rushstack-heft-0.34.0.tgz} name: '@rushstack/heft' - version: 0.33.0 + version: 0.34.0 engines: {node: '>=10.13.0'} hasBin: true dependencies: diff --git a/common/changes/@microsoft/api-extractor/export-star-as-support_2020-03-25-13-53.json b/common/changes/@microsoft/api-extractor/export-star-as-support_2020-03-25-13-53.json new file mode 100644 index 00000000000..dfb20e6e7ce --- /dev/null +++ b/common/changes/@microsoft/api-extractor/export-star-as-support_2020-03-25-13-53.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Added support for \"import * as module from './local/module';\" (GitHub #1029) -- Big thanks to @adventure-yunfei, @mckn, @rbuckton, and @octogonz who all helped with this difficult PR!", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "mckn@users.noreply.github.com" +} diff --git a/common/changes/@microsoft/api-extractor/export-star-as-support_2021-06-26-22-06.json b/common/changes/@microsoft/api-extractor/export-star-as-support_2021-06-26-22-06.json new file mode 100644 index 00000000000..119fff65bef --- /dev/null +++ b/common/changes/@microsoft/api-extractor/export-star-as-support_2021-06-26-22-06.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Include /// directives in API report", + "type": "patch" + } + ], + "packageName": "@microsoft/api-extractor", + "email": "4673363+octogonz@users.noreply.github.com" +} \ No newline at end of file