diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index 33192aa2e25..66817f7f38c 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -158,8 +158,10 @@ interface IExtractorConfigParameters { apiReportEnabled: boolean; reportFilePath: string; reportTempFilePath: string; + apiReportIncludeForgottenExports: boolean; docModelEnabled: boolean; apiJsonFilePath: string; + docModelIncludeForgottenExports: boolean; rollupEnabled: boolean; untrimmedFilePath: string; alphaTrimmedFilePath: string; @@ -246,11 +248,15 @@ export class ExtractorConfig { public readonly reportFilePath: string; /** The `reportTempFolder` path combined with the `reportFileName`. */ public readonly reportTempFilePath: string; + /** {@inheritDoc IConfigApiReport.includeForgottenExports} */ + public readonly apiReportIncludeForgottenExports: boolean; /** {@inheritDoc IConfigDocModel.enabled} */ public readonly docModelEnabled: boolean; /** {@inheritDoc IConfigDocModel.apiJsonFilePath} */ public readonly apiJsonFilePath: string; + /** {@inheritDoc IConfigDocModel.includeForgottenExports} */ + public readonly docModelIncludeForgottenExports: boolean; /** {@inheritDoc IConfigDtsRollup.enabled} */ public readonly rollupEnabled: boolean; @@ -307,8 +313,10 @@ export class ExtractorConfig { this.apiReportEnabled = parameters.apiReportEnabled; this.reportFilePath = parameters.reportFilePath; this.reportTempFilePath = parameters.reportTempFilePath; + this.apiReportIncludeForgottenExports = parameters.apiReportIncludeForgottenExports; this.docModelEnabled = parameters.docModelEnabled; this.apiJsonFilePath = parameters.apiJsonFilePath; + this.docModelIncludeForgottenExports = parameters.docModelIncludeForgottenExports; this.rollupEnabled = parameters.rollupEnabled; this.untrimmedFilePath = parameters.untrimmedFilePath; this.alphaTrimmedFilePath = parameters.alphaTrimmedFilePath; @@ -848,6 +856,7 @@ export class ExtractorConfig { let apiReportEnabled: boolean = false; let reportFilePath: string = ''; let reportTempFilePath: string = ''; + let apiReportIncludeForgottenExports: boolean = false; if (configObject.apiReport) { apiReportEnabled = !!configObject.apiReport.enabled; @@ -879,10 +888,12 @@ export class ExtractorConfig { reportFilePath = path.join(reportFolder, reportFilename); reportTempFilePath = path.join(reportTempFolder, reportFilename); + apiReportIncludeForgottenExports = !!configObject.apiReport.includeForgottenExports; } let docModelEnabled: boolean = false; let apiJsonFilePath: string = ''; + let docModelIncludeForgottenExports: boolean = false; if (configObject.docModel) { docModelEnabled = !!configObject.docModel.enabled; apiJsonFilePath = ExtractorConfig._resolvePathWithTokens( @@ -890,6 +901,7 @@ export class ExtractorConfig { configObject.docModel.apiJsonFilePath, tokenContext ); + docModelIncludeForgottenExports = !!configObject.docModel.includeForgottenExports; } let tsdocMetadataEnabled: boolean = false; @@ -993,8 +1005,10 @@ export class ExtractorConfig { apiReportEnabled, reportFilePath, reportTempFilePath, + apiReportIncludeForgottenExports, docModelEnabled, apiJsonFilePath, + docModelIncludeForgottenExports, rollupEnabled, untrimmedFilePath, alphaTrimmedFilePath, diff --git a/apps/api-extractor/src/api/ExtractorMessageId.ts b/apps/api-extractor/src/api/ExtractorMessageId.ts index 277fc678aeb..9af37df1ff9 100644 --- a/apps/api-extractor/src/api/ExtractorMessageId.ts +++ b/apps/api-extractor/src/api/ExtractorMessageId.ts @@ -28,7 +28,7 @@ export const enum ExtractorMessageId { IncompatibleReleaseTags = 'ae-incompatible-release-tags', /** - * "___ is exported by the package, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)." + * "___ is part of the package's API, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)." */ MissingReleaseTag = 'ae-missing-release-tag', diff --git a/apps/api-extractor/src/api/IConfigFile.ts b/apps/api-extractor/src/api/IConfigFile.ts index a07c3118aeb..fffe284e385 100644 --- a/apps/api-extractor/src/api/IConfigFile.ts +++ b/apps/api-extractor/src/api/IConfigFile.ts @@ -96,6 +96,17 @@ export interface IConfigApiReport { * prepend a folder token such as ``. */ reportTempFolder?: string; + + /** + * Whether "forgotten exports" should be included in the API report file. + * + * @remarks + * Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See + * https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more. + * + * @defaultValue `false` + */ + includeForgottenExports?: boolean; } /** @@ -120,6 +131,17 @@ export interface IConfigDocModel { * prepend a folder token such as ``. */ apiJsonFilePath?: string; + + /** + * Whether "forgotten exports" should be included in the doc model file. + * + * @remarks + * Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See + * https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more. + * + * @defaultValue `false` + */ + includeForgottenExports?: boolean; } /** @@ -376,7 +398,7 @@ export interface IConfigFile { testMode?: boolean; /** - * Specifies how API Extractor sorts members of an enum when generating api.json. + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. * * @remarks * By default, the output files will be sorted alphabetically, which is "by-name". diff --git a/apps/api-extractor/src/collector/Collector.ts b/apps/api-extractor/src/collector/Collector.ts index 86fcd1eee16..06420065f3a 100644 --- a/apps/api-extractor/src/collector/Collector.ts +++ b/apps/api-extractor/src/collector/Collector.ts @@ -246,28 +246,23 @@ export class Collector { this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment; } - const exportedAstEntities: AstEntity[] = []; - - // Create a CollectorEntity for each top-level export - const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint); + // Create a CollectorEntity for each top-level export. + const processedAstEntities: AstEntity[] = []; for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) { this._createCollectorEntity(astEntity, exportName); - - exportedAstEntities.push(astEntity); + processedAstEntities.push(astEntity); } - // Create a CollectorEntity for each indirectly referenced export. - // Note that we do this *after* the above loop, so that references to exported AstSymbols - // are encountered first as exports. - const alreadySeenAstSymbols: Set = new Set(); - for (const exportedAstEntity of exportedAstEntities) { - this._createEntityForIndirectReferences(exportedAstEntity, alreadySeenAstSymbols); - - if (exportedAstEntity instanceof AstSymbol) { - this.fetchSymbolMetadata(exportedAstEntity); + // Recursively create the remaining CollectorEntities after the top-level entities + // have been processed. + const alreadySeenAstEntities: Set = new Set(); + for (const astEntity of processedAstEntities) { + this._recursivelyCreateEntities(astEntity, alreadySeenAstEntities); + if (astEntity instanceof AstSymbol) { + this.fetchSymbolMetadata(astEntity); } } @@ -414,7 +409,11 @@ export class Collector { return overloadIndex; } - private _createCollectorEntity(astEntity: AstEntity, exportedName: string | undefined): CollectorEntity { + private _createCollectorEntity( + astEntity: AstEntity, + exportName?: string, + parent?: CollectorEntity + ): CollectorEntity { let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity); if (!entity) { @@ -425,50 +424,54 @@ export class Collector { this._collectReferenceDirectives(astEntity); } - if (exportedName) { - entity.addExportName(exportedName); + if (exportName) { + if (parent) { + entity.addLocalExportName(exportName, parent); + } else { + entity.addExportName(exportName); + } } return entity; } - private _createEntityForIndirectReferences( - astEntity: AstEntity, - alreadySeenAstEntities: Set - ): void { - if (alreadySeenAstEntities.has(astEntity)) { - return; - } + private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set): void { + if (alreadySeenAstEntities.has(astEntity)) return; alreadySeenAstEntities.add(astEntity); if (astEntity instanceof AstSymbol) { astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { for (const referencedAstEntity of astDeclaration.referencedAstEntities) { if (referencedAstEntity instanceof AstSymbol) { - // We only create collector entities for root-level symbols. - // For example, if a symbols is nested inside a namespace, only the root-level namespace - // get a collector entity + // We only create collector entities for root-level symbols. For example, if a symbol is + // nested inside a namespace, only the namespace gets a collector entity. Note that this + // is not true for AstNamespaceImports below. if (referencedAstEntity.parentAstSymbol === undefined) { - this._createCollectorEntity(referencedAstEntity, undefined); + this._createCollectorEntity(referencedAstEntity); } } else { - this._createCollectorEntity(referencedAstEntity, undefined); + this._createCollectorEntity(referencedAstEntity); } - this._createEntityForIndirectReferences(referencedAstEntity, alreadySeenAstEntities); + this._recursivelyCreateEntities(referencedAstEntity, alreadySeenAstEntities); } }); } if (astEntity instanceof AstNamespaceImport) { const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this); + const parentEntity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity); + if (!parentEntity) { + // This should never happen, as we've already created entities for all AstNamespaceImports. + throw new InternalError( + `Failed to get CollectorEntity for AstNamespaceImport with namespace name "${astEntity.namespaceName}"` + ); + } - 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); + for (const [localExportName, localAstEntity] of astModuleExportInfo.exportedLocalEntities) { + // Create a CollectorEntity for each local export within an AstNamespaceImport entity. + this._createCollectorEntity(localAstEntity, localExportName, parentEntity); + this._recursivelyCreateEntities(localAstEntity, alreadySeenAstEntities); } } } @@ -812,16 +815,22 @@ export class Collector { if (options.effectiveReleaseTag === ReleaseTag.None) { if (!astDeclaration.astSymbol.isExternal) { // for now, don't report errors for external code - // Don't report missing release tags for forgotten exports + // Don't report missing release tags for forgotten exports (unless we're including forgotten exports + // in either the API report or doc model). const astSymbol: AstSymbol = astDeclaration.astSymbol; const entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astSymbol.rootAstSymbol); - if (entity && entity.consumable) { + if ( + entity && + (entity.consumable || + this.extractorConfig.apiReportIncludeForgottenExports || + this.extractorConfig.docModelIncludeForgottenExports) + ) { // 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') { this.messageRouter.addAnalyzerIssue( ExtractorMessageId.MissingReleaseTag, - `"${entity.astEntity.localName}" is exported by the package, but it is missing ` + + `"${entity.astEntity.localName}" is part of the package's API, but it is missing ` + `a release tag (@alpha, @beta, @public, or @internal)`, astSymbol ); diff --git a/apps/api-extractor/src/collector/CollectorEntity.ts b/apps/api-extractor/src/collector/CollectorEntity.ts index 23f57db2c6b..1ec6e772a5b 100644 --- a/apps/api-extractor/src/collector/CollectorEntity.ts +++ b/apps/api-extractor/src/collector/CollectorEntity.ts @@ -7,7 +7,6 @@ import { AstSymbol } from '../analyzer/AstSymbol'; import { Collector } from './Collector'; import { Sort } from '@rushstack/node-core-library'; 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. @@ -27,21 +26,20 @@ export class CollectorEntity { private _exportNames: Set = new Set(); private _exportNamesSorted: boolean = false; private _singleExportName: string | undefined = undefined; + private _localExportNamesByParent: Map> = new Map(); private _nameForEmit: string | undefined = undefined; private _sortKey: string | undefined = undefined; - private _astNamespaceImports: Set = new Set(); - public constructor(astEntity: AstEntity) { this.astEntity = astEntity; } /** - * The declaration name that will be emitted in a .d.ts rollup. For non-exported declarations, - * Collector._makeUniqueNames() may need to rename the declaration to avoid conflicts with other declarations - * in that module. + * The declaration name that will be emitted in the .d.ts rollup, .api.md, and .api.json files. Generated by + * `Collector._makeUniqueNames`. Be aware that the declaration may be renamed to avoid conflicts with (1) + * global names (e.g. `Promise`) and (2) if local, other local names across different files. */ public get nameForEmit(): string | undefined { return this._nameForEmit; @@ -53,7 +51,7 @@ export class CollectorEntity { } /** - * If this symbol is exported from the entry point, the list of export names. + * The list of export names if this symbol is exported from the entry point. * * @remarks * Note that a given symbol may be exported more than once: @@ -98,56 +96,101 @@ export class CollectorEntity { } /** - * Returns true if this symbol is an export for the entry point being analyzed. + * Indicates that this entity is exported from its parent module (i.e. either the package entry point or + * a local namespace). Compare to `CollectorEntity.consumable`. + * + * @remarks + * In the example below: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * ``` + * + * Namespace `calculator` is neither exported nor consumable, function `add` is exported (from `calculator`) + * but not consumable. */ public get exported(): boolean { - return this.exportNames.size > 0; + // Exported from top-level? + if (this.exportNames.size > 0) return true; + + // Exported from parent? + for (const localExportNames of this._localExportNamesByParent.values()) { + if (localExportNames.size > 0) { + return true; + } + } + + return false; } /** - * 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. + * Indicates that it is possible for a consumer of the API to "consume" this entity, either by importing + * it directly or via a namespace. If an entity is not consumable, then API Extractor will report an + * `ae-forgotten-export` warning. Compare to `CollectorEntity.exported`. * * @remarks - * Generally speaking, an API item is consumable if: + * 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) + * 1. It is exported from the top-level entry point OR + * 2. It is exported from a consumable parent entity. * - * - AND it is exported by the entry point - * - * However a special case occurs with `AstNamespaceImport` which produces a rollup like this: + * For an example of #2, consider how `AstNamespaceImport` entities are processed. A generated rollup.d.ts + * might look like this: * * ```ts - * declare interface IForgottenExport { } - * - * declare function member(): IForgottenExport; - * - * declare namespace ns { + * declare function add(): void; + * declare namespace calculator { * export { - * member + * add * } * } - * export { ns } + * export { calculator } * ``` * - * In this example, `IForgottenExport` is not consumable. Whereas `member()` is consumable as `ns.member()` - * even though `member()` itself is not exported. + * In this example, `add` is exported via the consumable `calculator` namespace. */ public get consumable(): boolean { - return this.exported || this._astNamespaceImports.size > 0; + // Exported from top-level? + if (this.exportNames.size > 0) return true; + + // Exported from consumable parent? + for (const [parent, localExportNames] of this._localExportNamesByParent) { + if (localExportNames.size > 0 && parent.consumable) { + return true; + } + } + + return false; } /** - * Associates this entity with a `AstNamespaceImport`. + * Whether the entity has any parent entities. + * + * @remarks + * In the example below: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * ``` + * + * The `CollectorEntity` for `calculator` is the parent of the `CollectorEntity` for `add`. */ - public addAstNamespaceImports(astNamespaceImport: AstNamespaceImport): void { - this._astNamespaceImports.add(astNamespaceImport); + public get hasParents(): boolean { + return this._localExportNamesByParent.size > 0; } /** - * Adds a new exportName to the exportNames set. + * Adds a new export name to the entity. */ public addExportName(exportName: string): void { if (!this._exportNames.has(exportName)) { @@ -162,6 +205,30 @@ export class CollectorEntity { } } + /** + * Adds a new local export name to the entity. + * + * @remarks + * In the example below: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * ``` + * + * `add` is the local export name for the `CollectorEntity` for `add`. + */ + public addLocalExportName(localExportName: string, parent: CollectorEntity): void { + const localExportNames: Set = this._localExportNamesByParent.get(parent) || new Set(); + localExportNames.add(localExportName); + + this._localExportNamesByParent.set(parent, localExportNames); + } + /** * A sorting key used by DtsRollupGenerator._makeUniqueNames() */ diff --git a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts index ee7637d0161..ba96c9db803 100644 --- a/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts +++ b/apps/api-extractor/src/enhancers/DocCommentEnhancer.ts @@ -28,7 +28,11 @@ export class DocCommentEnhancer { public analyze(): void { for (const entity of this._collector.entities) { if (entity.astEntity instanceof AstSymbol) { - if (entity.consumable) { + if ( + entity.consumable || + this._collector.extractorConfig.apiReportIncludeForgottenExports || + this._collector.extractorConfig.docModelIncludeForgottenExports + ) { 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 302480ee86d..8795a4ec1ed 100644 --- a/apps/api-extractor/src/enhancers/ValidationEnhancer.ts +++ b/apps/api-extractor/src/enhancers/ValidationEnhancer.ts @@ -21,7 +21,13 @@ export class ValidationEnhancer { const alreadyWarnedEntities: Set = new Set(); for (const entity of collector.entities) { - if (!entity.consumable) { + if ( + !( + entity.consumable || + collector.extractorConfig.apiReportIncludeForgottenExports || + collector.extractorConfig.docModelIncludeForgottenExports + ) + ) { continue; } @@ -73,7 +79,7 @@ export class ValidationEnhancer { if (symbolMetadata.maxEffectiveReleaseTag === ReleaseTag.Internal) { if (!astSymbol.parentAstSymbol) { - // If it's marked as @internal and has no parent, then it needs and underscore. + // If it's marked as @internal and has no parent, then it needs an underscore. // We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this: // // /** @public */ @@ -210,9 +216,8 @@ export class ValidationEnhancer { continue; } - localName = rootSymbol.localName; - collectorEntity = collector.tryGetCollectorEntity(rootSymbol); + localName = collectorEntity?.nameForEmit || rootSymbol.localName; const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity); referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag; @@ -222,7 +227,7 @@ export class ValidationEnhancer { // TODO: Currently the "import * as ___ from ___" syntax does not yet support doc comments referencedReleaseTag = ReleaseTag.Public; - localName = referencedEntity.localName; + localName = collectorEntity?.nameForEmit || referencedEntity.localName; } else { continue; } @@ -233,7 +238,7 @@ export class ValidationEnhancer { ExtractorMessageId.IncompatibleReleaseTags, `The symbol "${astDeclaration.astSymbol.localName}"` + ` is marked as ${ReleaseTag.getTagName(declarationReleaseTag)},` + - ` but its signature references "${referencedEntity.localName}"` + + ` but its signature references "${localName}"` + ` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`, astDeclaration ); diff --git a/apps/api-extractor/src/generators/ApiModelGenerator.ts b/apps/api-extractor/src/generators/ApiModelGenerator.ts index 8d7e9df895a..8c7cc7cbcbe 100644 --- a/apps/api-extractor/src/generators/ApiModelGenerator.ts +++ b/apps/api-extractor/src/generators/ApiModelGenerator.ts @@ -35,6 +35,7 @@ import { } from '@microsoft/api-extractor-model'; import { Collector } from '../collector/Collector'; +import { CollectorEntity } from '../collector/CollectorEntity'; import { AstDeclaration } from '../analyzer/AstDeclaration'; import { ExcerptBuilder, IExcerptBuilderNodeToCapture } from './ExcerptBuilder'; import { AstSymbol } from '../analyzer/AstSymbol'; @@ -45,6 +46,7 @@ import { AstNamespaceImport } from '../analyzer/AstNamespaceImport'; import { AstEntity } from '../analyzer/AstEntity'; import { AstModule } from '../analyzer/AstModule'; import { TypeScriptInternals } from '../analyzer/TypeScriptInternals'; +import { InternalError } from '@rushstack/node-core-library'; export class ApiModelGenerator { private readonly _collector: Collector; @@ -74,9 +76,13 @@ export class ApiModelGenerator { const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: '' }); apiPackage.addMember(apiEntryPoint); - // Create a CollectorEntity for each top-level export for (const entity of this._collector.entities) { - if (entity.exported) { + // Note that we don't process entities that have parents, because those entities will be recursively + // processed by the parent. + if ( + !entity.hasParents && + (entity.consumable || this._collector.extractorConfig.docModelIncludeForgottenExports) + ) { this._processAstEntity(entity.astEntity, entity.nameForEmit, apiEntryPoint); } } @@ -108,11 +114,11 @@ export class ApiModelGenerator { // 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()./ + // 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); + this._processAstNamespaceImport(astEntity, exportedName, parentApiItem); return; } @@ -121,13 +127,15 @@ export class ApiModelGenerator { // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. } - private _processAstModule( - astModule: AstModule, + private _processAstNamespaceImport( + astNamespaceImport: AstNamespaceImport, exportedName: string | undefined, parentApiItem: ApiItemContainerMixin ): void { + const astModule: AstModule = astNamespaceImport.astModule; const name: string = exportedName ? exportedName : astModule.moduleSymbol.name; const containerKey: string = ApiNamespace.getContainerKey(name); + const isExported: boolean = this._isExported(astNamespaceImport); let apiNamespace: ApiNamespace | undefined = parentApiItem.tryGetMemberByKey( containerKey @@ -138,7 +146,8 @@ export class ApiModelGenerator { name, docComment: undefined, releaseTag: ReleaseTag.None, - excerptTokens: [] + excerptTokens: [], + isExported }); parentApiItem.addMember(apiNamespace); } @@ -387,6 +396,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); apiClass = new ApiClass({ name, @@ -395,7 +405,8 @@ export class ApiModelGenerator { excerptTokens, typeParameters, extendsTokenRange, - implementsTokenRanges + implementsTokenRanges, + isExported }); parentApiItem.addMember(apiClass); @@ -471,8 +482,9 @@ export class ApiModelGenerator { const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const preserveMemberOrder: boolean = this._collector.extractorConfig.enumMemberOrder === EnumMemberOrder.Preserve; + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); - apiEnum = new ApiEnum({ name, docComment, releaseTag, excerptTokens, preserveMemberOrder }); + apiEnum = new ApiEnum({ name, docComment, releaseTag, excerptTokens, preserveMemberOrder, isExported }); parentApiItem.addMember(apiEnum); } @@ -554,9 +566,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; - if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { - return; // trim out items marked as "@internal" or "@alpha" - } + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); apiFunction = new ApiFunction({ name, @@ -566,7 +576,8 @@ export class ApiModelGenerator { parameters, overloadIndex, excerptTokens, - returnTypeTokenRange + returnTypeTokenRange, + isExported }); parentApiItem.addMember(apiFunction); @@ -658,6 +669,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); apiInterface = new ApiInterface({ name, @@ -665,7 +677,8 @@ export class ApiModelGenerator { releaseTag, excerptTokens, typeParameters, - extendsTokenRanges + extendsTokenRanges, + isExported }); parentApiItem.addMember(apiInterface); @@ -806,8 +819,9 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); - apiNamespace = new ApiNamespace({ name, docComment, releaseTag, excerptTokens }); + apiNamespace = new ApiNamespace({ name, docComment, releaseTag, excerptTokens, isExported }); parentApiItem.addMember(apiNamespace); } @@ -955,6 +969,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); apiTypeAlias = new ApiTypeAlias({ name, @@ -962,7 +977,8 @@ export class ApiModelGenerator { typeParameters, releaseTag, excerptTokens, - typeTokenRange + typeTokenRange, + isExported }); parentApiItem.addMember(apiTypeAlias); @@ -1000,6 +1016,7 @@ export class ApiModelGenerator { const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isReadonly: boolean = this._isReadonly(astDeclaration); + const isExported: boolean = this._isExported(astDeclaration.astSymbol.rootAstSymbol); apiVariable = new ApiVariable({ name, @@ -1008,7 +1025,8 @@ export class ApiModelGenerator { excerptTokens, variableTypeTokenRange, initializerTokenRange, - isReadonly + isReadonly, + isExported }); parentApiItem.addMember(apiVariable); @@ -1113,4 +1131,16 @@ export class ApiModelGenerator { } } } + + private _isExported(astEntity: AstEntity): boolean { + // Collector entities are only created for root symbols. + const entity: CollectorEntity | undefined = this._collector.tryGetCollectorEntity(astEntity); + + if (!entity) { + // This should never happen. + throw new InternalError(`Failed to get collector entity for AstEntity "${astEntity.localName}"`); + } + + return entity.exported; + } } diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index 4f927135a4c..6c99bdee3db 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -78,7 +78,7 @@ export class ApiReportGenerator { // Emit the regular declarations for (const entity of collector.entities) { const astEntity: AstEntity = entity.astEntity; - if (entity.consumable) { + if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) { // 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. @@ -146,7 +146,7 @@ export class ApiReportGenerator { 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` + + `The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` + SourceFileLocationFormatter.formatDeclaration(astEntity.declaration) ); } diff --git a/apps/api-extractor/src/schemas/api-extractor-defaults.json b/apps/api-extractor/src/schemas/api-extractor-defaults.json index a6f1afbe4dc..d3a949e4c3f 100644 --- a/apps/api-extractor/src/schemas/api-extractor-defaults.json +++ b/apps/api-extractor/src/schemas/api-extractor-defaults.json @@ -18,12 +18,14 @@ // ("enabled" is required) "reportFileName": ".api.md", "reportFolder": "/etc/", - "reportTempFolder": "/temp/" + "reportTempFolder": "/temp/", + "includeForgottenExports": false }, "docModel": { // ("enabled" is required) - "apiJsonFilePath": "/temp/.api.json" + "apiJsonFilePath": "/temp/.api.json", + "includeForgottenExports": false }, "dtsRollup": { diff --git a/apps/api-extractor/src/schemas/api-extractor-template.json b/apps/api-extractor/src/schemas/api-extractor-template.json index 0dcdc009009..62f4fd32442 100644 --- a/apps/api-extractor/src/schemas/api-extractor-template.json +++ b/apps/api-extractor/src/schemas/api-extractor-template.json @@ -80,12 +80,13 @@ // "testMode": false, /** - * Specifies how API Extractor sorts members of an enum when generating api.json. By default, the output files - * will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify "preserve". + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". * * DEFAULT VALUE: "by-name" */ - // enumMemberOrder?: EnumMemberOrder, + // "enumMemberOrder": "by-name", /** * Determines how the TypeScript compiler engine will be invoked by API Extractor. @@ -175,7 +176,16 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/" */ - // "reportTempFolder": "/temp/" + // "reportTempFolder": "/temp/", + + /** + * Whether "forgotten exports" should be included in the API report file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false }, /** @@ -196,7 +206,16 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/.api.json" */ - // "apiJsonFilePath": "/temp/.api.json" + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false }, /** diff --git a/apps/api-extractor/src/schemas/api-extractor.schema.json b/apps/api-extractor/src/schemas/api-extractor.schema.json index 1556487f0b3..14d18a4f6cf 100644 --- a/apps/api-extractor/src/schemas/api-extractor.schema.json +++ b/apps/api-extractor/src/schemas/api-extractor.schema.json @@ -32,7 +32,7 @@ }, "enumMemberOrder": { - "description": "Specifies how API Extractor sorts the members of an enum when generating the .api.json doc model. \n 'by-name': sort the items according to the enum member name \n 'preserve': keep the original order that items appear in the source code", + "description": "Specifies how API Extractor sorts the members of an enum when generating the .api.json doc model. \n 'by-name': sort the items according to the enum member name \n 'preserve': keep the original order that items appear in the source code", "type": "string", "enum": ["by-name", "preserve"], "default": "by-name" @@ -80,6 +80,11 @@ "reportTempFolder": { "description": "Specifies the folder where the temporary report file is written. The file name portion is determined by the \"reportFileName\" setting. After the temporary file is written to disk, it is compared with the file in the \"reportFolder\". If they are different, a production build will fail. The path is resolved relative to the folder of the config file that contains the setting; to change this, prepend a folder token such as \"\".", "type": "string" + }, + + "includeForgottenExports": { + "description": "Whether \"forgotten exports\" should be included in the API report file. Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.", + "type": "boolean" } }, "required": ["enabled"], @@ -97,6 +102,11 @@ "apiJsonFilePath": { "description": "The output path for the doc model file. The file extension should be \".api.json\". The path is resolved relative to the folder of the config file that contains the setting; to change this, prepend a folder token such as \"\".", "type": "string" + }, + + "includeForgottenExports": { + "description": "Whether \"forgotten exports\" should be included in the doc model file. Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.", + "type": "boolean" } }, "required": ["enabled"], diff --git a/build-tests/api-extractor-lib1-test/etc/api-extractor-lib1-test.api.md b/build-tests/api-extractor-lib1-test/etc/api-extractor-lib1-test.api.md index d7856bac80f..ae7e32783b4 100644 --- a/build-tests/api-extractor-lib1-test/etc/api-extractor-lib1-test.api.md +++ b/build-tests/api-extractor-lib1-test/etc/api-extractor-lib1-test.api.md @@ -37,5 +37,4 @@ export namespace Lib1Namespace { } } - ``` diff --git a/build-tests/api-extractor-scenarios/config/build-config.json b/build-tests/api-extractor-scenarios/config/build-config.json index b1c0327f4e7..9501b49e9f2 100644 --- a/build-tests/api-extractor-scenarios/config/build-config.json +++ b/build-tests/api-extractor-scenarios/config/build-config.json @@ -33,6 +33,7 @@ "functionOverload", "importEquals", "importType", + "includeForgottenExports", "inconsistentReleaseTags", "internationalCharacters", "mergedDeclarations", diff --git a/build-tests/api-extractor-scenarios/etc/ambientNameConflict/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/ambientNameConflict/api-extractor-scenarios.api.md index 5c7a3d96b52..570e8a2cc45 100644 --- a/build-tests/api-extractor-scenarios/etc/ambientNameConflict/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/ambientNameConflict/api-extractor-scenarios.api.md @@ -4,7 +4,7 @@ ```ts -// Warning: (ae-forgotten-export) The symbol "Promise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Promise_2" needs to be exported by the entry point index.d.ts // // @public (undocumented) export function ambientNameConflict(p1: Promise, p2: Promise_2): void; diff --git a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.json index 0c8f7cfee5f..98e888539c8 100644 --- a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.json +++ b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.json @@ -243,12 +243,12 @@ }, { "kind": "Variable", - "canonicalReference": "api-extractor-scenarios!calculator.calucatorVersion:var", + "canonicalReference": "api-extractor-scenarios!calculator.calculatorVersion:var", "docComment": "/**\n * Returns the version of the calculator.\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "calucatorVersion: " + "text": "calculatorVersion: " }, { "kind": "Content", @@ -257,7 +257,7 @@ ], "isReadonly": true, "releaseTag": "Public", - "name": "calucatorVersion", + "name": "calculatorVersion", "variableTypeTokenRange": { "startIndex": 1, "endIndex": 2 @@ -396,12 +396,12 @@ }, { "kind": "Variable", - "canonicalReference": "api-extractor-scenarios!calculator2.calucatorVersion:var", + "canonicalReference": "api-extractor-scenarios!calculator2.calculatorVersion:var", "docComment": "/**\n * Returns the version of the calculator.\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "calucatorVersion: " + "text": "calculatorVersion: " }, { "kind": "Content", @@ -410,7 +410,7 @@ ], "isReadonly": true, "releaseTag": "Public", - "name": "calucatorVersion", + "name": "calculatorVersion", "variableTypeTokenRange": { "startIndex": 1, "endIndex": 2 diff --git a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.md index 6c1cc0cec80..a05e6f34a4a 100644 --- a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/api-extractor-scenarios.api.md @@ -14,7 +14,7 @@ declare namespace calculator { export { add, subtract, - calucatorVersion + calculatorVersion } } export { calculator } @@ -23,13 +23,13 @@ declare namespace calculator2 { export { add_2 as add, subtract_2 as subtract, - calucatorVersion + calculatorVersion } } export { calculator2 } // @public -const calucatorVersion: string; +const calculatorVersion: string; // @beta function subtract(a: number, b: number): number; diff --git a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/rollup.d.ts index bd8c3300e64..042af626326 100644 --- a/build-tests/api-extractor-scenarios/etc/exportImportStarAs/rollup.d.ts +++ b/build-tests/api-extractor-scenarios/etc/exportImportStarAs/rollup.d.ts @@ -20,7 +20,7 @@ declare namespace calculator { export { add, subtract, - calucatorVersion + calculatorVersion } } export { calculator } @@ -29,7 +29,7 @@ declare namespace calculator2 { export { add_2 as add, subtract_2 as subtract, - calucatorVersion + calculatorVersion } } export { calculator2 } @@ -38,7 +38,7 @@ export { calculator2 } * Returns the version of the calculator. * @public */ -declare const calucatorVersion: string; +declare const calculatorVersion: string; /** * Returns the sum of subtracting `b` from `a` diff --git a/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.md index 0acea2fe3d6..be6c99dbe02 100644 --- a/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.md +++ b/build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.md @@ -9,10 +9,6 @@ // @public (undocumented) function exportedApi(): forgottenNs.ForgottenClass; -// @public (undocumented) -class ForgottenClass { -} - declare namespace ns { export { exportedApi diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json new file mode 100644 index 00000000000..95a94902c4a --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.json @@ -0,0 +1,638 @@ +{ + "metadata": { + "toolPackage": "@microsoft/api-extractor", + "toolVersion": "[test mode]", + "schemaVersion": 1009, + "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 + }, + "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": "Class", + "canonicalReference": "api-extractor-scenarios!~AnotherDuplicateName_2:class", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare class AnotherDuplicateName " + } + ], + "releaseTag": "Public", + "name": "AnotherDuplicateName_2", + "preserveMemberOrder": false, + "members": [], + "implementsTokenRanges": [] + }, + { + "kind": "Class", + "canonicalReference": "api-extractor-scenarios!~AnotherDuplicateName:class", + "docComment": "/**\n * This forgotten item has the same name as another forgotten item in another file. They should be given unique names.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare class AnotherDuplicateName " + } + ], + "releaseTag": "Public", + "name": "AnotherDuplicateName", + "preserveMemberOrder": false, + "members": [], + "implementsTokenRanges": [] + }, + { + "kind": "TypeAlias", + "canonicalReference": "api-extractor-scenarios!~DuplicateName_2:type", + "docComment": "/**\n * Will be renamed to avoid a name conflict with the exported `DuplicateName` from index.ts.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare type DuplicateName = " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "releaseTag": "Public", + "name": "DuplicateName_2", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "TypeAlias", + "canonicalReference": "api-extractor-scenarios!DuplicateName:type", + "docComment": "/**\n * This type is exported but has the same name as a forgotten type in './internal.ts'. This forgotten type is also included in the API report and doc model files. The forgotten type will be renamed to avoid a name conflict.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare type DuplicateName = " + }, + { + "kind": "Content", + "text": "boolean" + }, + { + "kind": "Content", + "text": ";" + } + ], + "releaseTag": "Public", + "name": "DuplicateName", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Class", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport1:class", + "docComment": "/**\n * `ForgottenExport2` wants to inherit this doc comment, but unfortunately this isn't supported yet\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare class ForgottenExport1 " + } + ], + "releaseTag": "Public", + "name": "ForgottenExport1", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Constructor", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport1:constructor(1)", + "docComment": "/**\n * Constructs a new instance of the `ForgottenExport1` class\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "constructor();" + } + ], + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [] + }, + { + "kind": "Property", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport1#prop:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "prop?: " + }, + { + "kind": "Reference", + "text": "ForgottenExport2", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport2:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "prop", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false + } + ], + "implementsTokenRanges": [] + }, + { + "kind": "TypeAlias", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport2:type", + "docComment": "/**\n * {@inheritDoc ForgottenExport1}\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare type ForgottenExport2 = " + }, + { + "kind": "Content", + "text": "number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "releaseTag": "Public", + "name": "ForgottenExport2", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport4:namespace", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "declare namespace ForgottenExport4 " + } + ], + "releaseTag": "Public", + "name": "ForgottenExport4", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Class", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport4~ForgottenExport5:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "class ForgottenExport5 " + } + ], + "releaseTag": "Public", + "name": "ForgottenExport5", + "preserveMemberOrder": false, + "members": [], + "implementsTokenRanges": [] + } + ] + }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!~internal2:namespace", + "docComment": "", + "excerptTokens": [], + "releaseTag": "None", + "name": "internal2", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Class", + "canonicalReference": "api-extractor-scenarios!~internal2.ForgottenExport6:class", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare class ForgottenExport6 " + } + ], + "releaseTag": "Public", + "name": "ForgottenExport6", + "preserveMemberOrder": false, + "members": [], + "implementsTokenRanges": [] + } + ] + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction1:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction1(): " + }, + { + "kind": "Reference", + "text": "ForgottenExport1", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport1:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction1" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction2:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction2(): " + }, + { + "kind": "Reference", + "text": "DuplicateName", + "canonicalReference": "api-extractor-scenarios!~DuplicateName:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction2" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction4:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction4(): " + }, + { + "kind": "Reference", + "text": "ForgottenExport4.ForgottenExport5", + "canonicalReference": "api-extractor-scenarios!~ForgottenExport4.ForgottenExport5:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction4" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction5:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction5(): " + }, + { + "kind": "Content", + "text": "internal2." + }, + { + "kind": "Reference", + "text": "ForgottenExport6", + "canonicalReference": "api-extractor-scenarios!ForgottenExport6:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction5" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction6:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction6(): " + }, + { + "kind": "Reference", + "text": "AnotherDuplicateName", + "canonicalReference": "api-extractor-scenarios!~AnotherDuplicateName:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction6" + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!someFunction7:function(1)", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function someFunction7(): " + }, + { + "kind": "Reference", + "text": "AnotherDuplicateName", + "canonicalReference": "api-extractor-scenarios!~AnotherDuplicateName:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction7" + }, + { + "kind": "Namespace", + "canonicalReference": "api-extractor-scenarios!SomeNamespace1:namespace", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare namespace SomeNamespace1 " + } + ], + "releaseTag": "Public", + "name": "SomeNamespace1", + "preserveMemberOrder": false, + "members": [ + { + "kind": "Class", + "canonicalReference": "api-extractor-scenarios!SomeNamespace1.ForgottenExport3:class", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "class ForgottenExport3 " + } + ], + "releaseTag": "Public", + "name": "ForgottenExport3", + "preserveMemberOrder": false, + "members": [], + "implementsTokenRanges": [] + }, + { + "kind": "Function", + "canonicalReference": "api-extractor-scenarios!SomeNamespace1.someFunction3:function(1)", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "export function someFunction3(): " + }, + { + "kind": "Reference", + "text": "ForgottenExport3", + "canonicalReference": "api-extractor-scenarios!SomeNamespace1~ForgottenExport3:class" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [], + "name": "someFunction3" + } + ] + } + ] + } + ] +} diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md new file mode 100644 index 00000000000..43157fe55bb --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/api-extractor-scenarios.api.md @@ -0,0 +1,94 @@ +## 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 +class AnotherDuplicateName { +} + +// @public (undocumented) +class AnotherDuplicateName_2 { +} + +// @public +export type DuplicateName = boolean; + +// @public +type DuplicateName_2 = number; + +// @public +class ForgottenExport1 { + constructor(); + // Warning: (ae-forgotten-export) The symbol "ForgottenExport2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + prop?: ForgottenExport2; +} + +// Warning: (ae-unresolved-inheritdoc-reference) The @inheritDoc reference could not be resolved: The package "api-extractor-scenarios" does not have an export "ForgottenExport1" +// +// @public (undocumented) +type ForgottenExport2 = number; + +// @public (undocumented) +namespace ForgottenExport4 { + // (undocumented) + class ForgottenExport5 { + } +} + +// @public (undocumented) +class ForgottenExport6 { +} + +declare namespace internal2 { + export { + ForgottenExport6 + } +} + +// Warning: (ae-forgotten-export) The symbol "ForgottenExport1" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction1(): ForgottenExport1; + +// Warning: (ae-forgotten-export) The symbol "DuplicateName_2" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction2(): DuplicateName_2; + +// Warning: (ae-forgotten-export) The symbol "ForgottenExport4" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction4(): ForgottenExport4.ForgottenExport5; + +// Warning: (ae-forgotten-export) The symbol "internal2" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction5(): internal2.ForgottenExport6; + +// Warning: (ae-forgotten-export) The symbol "AnotherDuplicateName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction6(): AnotherDuplicateName; + +// Warning: (ae-forgotten-export) The symbol "AnotherDuplicateName_2" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function someFunction7(): AnotherDuplicateName_2; + +// @public (undocumented) +export namespace SomeNamespace1 { + // (undocumented) + export class ForgottenExport3 { + } + // (undocumented) + export function someFunction3(): ForgottenExport3; + {}; +} + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts new file mode 100644 index 00000000000..8010e499625 --- /dev/null +++ b/build-tests/api-extractor-scenarios/etc/includeForgottenExports/rollup.d.ts @@ -0,0 +1,86 @@ +/** + * This forgotten item has the same name as another forgotten item in another + * file. They should be given unique names. + * @public + */ +declare class AnotherDuplicateName { +} + +/** @public */ +declare class AnotherDuplicateName_2 { +} + +/** + * This type is exported but has the same name as a forgotten type in './internal.ts'. This + * forgotten type is also included in the API report and doc model files. The forgotten type + * will be renamed to avoid a name conflict. + * @public + */ +export declare type DuplicateName = boolean; + +/** + * Will be renamed to avoid a name conflict with the exported `DuplicateName` from + * index.ts. + * @public + */ +declare type DuplicateName_2 = number; + +/** + * `ForgottenExport2` wants to inherit this doc comment, but unfortunately this isn't + * supported yet + * @public + */ +declare class ForgottenExport1 { + prop?: ForgottenExport2; + constructor(); +} + +/** + * @public + * {@inheritDoc ForgottenExport1} + */ +declare type ForgottenExport2 = number; + +/** @public */ +declare namespace ForgottenExport4 { + class ForgottenExport5 { + } +} + +/** @public */ +declare class ForgottenExport6 { +} + +declare namespace internal2 { + export { + ForgottenExport6 + } +} + +/** @public */ +export declare function someFunction1(): ForgottenExport1; + +/** @public */ +export declare function someFunction2(): DuplicateName_2; + +/** @public */ +export declare function someFunction4(): ForgottenExport4.ForgottenExport5; + +/** @public */ +export declare function someFunction5(): internal2.ForgottenExport6; + +/** @public */ +export declare function someFunction6(): AnotherDuplicateName; + +/** @public */ +export declare function someFunction7(): AnotherDuplicateName_2; + +/** @public */ +export declare namespace SomeNamespace1 { + export class ForgottenExport3 { + } + export function someFunction3(): ForgottenExport3; + {}; +} + +export { } diff --git a/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts b/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts index 1e4914ce34e..67d0445901d 100644 --- a/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts +++ b/build-tests/api-extractor-scenarios/src/exportImportStarAs/common.ts @@ -2,4 +2,4 @@ * Returns the version of the calculator. * @public */ -export const calucatorVersion: string = '1.0.0'; +export const calculatorVersion: string = '1.0.0'; diff --git a/build-tests/api-extractor-scenarios/src/includeForgottenExports/config/api-extractor-overrides.json b/build-tests/api-extractor-scenarios/src/includeForgottenExports/config/api-extractor-overrides.json new file mode 100644 index 00000000000..103f3c5ec14 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/includeForgottenExports/config/api-extractor-overrides.json @@ -0,0 +1,13 @@ +{ + "apiReport": { + "enabled": true, + "reportFolder": "/etc/includeForgottenExports", + "includeForgottenExports": true + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "/etc/includeForgottenExports/.api.json", + "includeForgottenExports": true + } +} diff --git a/build-tests/api-extractor-scenarios/src/includeForgottenExports/index.ts b/build-tests/api-extractor-scenarios/src/includeForgottenExports/index.ts new file mode 100644 index 00000000000..908200b2973 --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/includeForgottenExports/index.ts @@ -0,0 +1,70 @@ +import * as internal2 from './internal2'; + +/** + * `ForgottenExport2` wants to inherit this doc comment, but unfortunately this isn't + * supported yet + * @public + */ +class ForgottenExport1 { + prop?: ForgottenExport2; + constructor() {} +} + +/** + * @public + * {@inheritDoc ForgottenExport1} + */ +type ForgottenExport2 = number; + +/** @public */ +export function someFunction1(): ForgottenExport1 { + return new ForgottenExport1(); +} + +/** + * This type is exported but has the same name as a forgotten type in './internal.ts'. This + * forgotten type is also included in the API report and doc model files. The forgotten type + * will be renamed to avoid a name conflict. + * @public + */ +export type DuplicateName = boolean; + +export { someFunction2 } from './internal1'; + +/** @public */ +export namespace SomeNamespace1 { + class ForgottenExport3 {} + + export function someFunction3(): ForgottenExport3 { + return new ForgottenExport3(); + } +} + +/** @public */ +namespace ForgottenExport4 { + export class ForgottenExport5 {} +} + +/** @public */ +export function someFunction4(): ForgottenExport4.ForgottenExport5 { + return new ForgottenExport4.ForgottenExport5(); +} + +/** @public */ +export function someFunction5(): internal2.ForgottenExport6 { + return new internal2.ForgottenExport6(); +} + +/** + * This forgotten item has the same name as another forgotten item in another + * file. They should be given unique names. + * @public + */ +class AnotherDuplicateName {} + +/** @public */ +export function someFunction6(): AnotherDuplicateName { + return new AnotherDuplicateName(); +} + +export { someFunction7 } from './internal1'; diff --git a/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal1.ts b/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal1.ts new file mode 100644 index 00000000000..e9adb34de2e --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal1.ts @@ -0,0 +1,19 @@ +/** + * Will be renamed to avoid a name conflict with the exported `DuplicateName` from + * index.ts. + * @public + */ +type DuplicateName = number; + +/** @public */ +export function someFunction2(): DuplicateName { + return 5; +} + +/** @public */ +class AnotherDuplicateName {} + +/** @public */ +export function someFunction7(): AnotherDuplicateName { + return new AnotherDuplicateName(); +} diff --git a/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal2.ts b/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal2.ts new file mode 100644 index 00000000000..248395af66b --- /dev/null +++ b/build-tests/api-extractor-scenarios/src/includeForgottenExports/internal2.ts @@ -0,0 +1,2 @@ +/** @public */ +export class ForgottenExport6 {} 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 87e44be72c0..395686f0c86 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 @@ -110,7 +110,7 @@ export class ForgottenExportConsumer1 { // @public (undocumented) export class ForgottenExportConsumer2 { - // Warning: (ae-forgotten-export) The symbol "IForgottenExport" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IForgottenExport_2" needs to be exported by the entry point index.d.ts // // (undocumented) test2(): IForgottenExport_2 | undefined; @@ -203,5 +203,4 @@ export const VARIABLE: string; // @public export function virtual(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor): void; - ``` diff --git a/common/changes/@microsoft/api-extractor-model/exported_2022-07-26-16-12.json b/common/changes/@microsoft/api-extractor-model/exported_2022-07-26-16-12.json new file mode 100644 index 00000000000..9d4cc4a1201 --- /dev/null +++ b/common/changes/@microsoft/api-extractor-model/exported_2022-07-26-16-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor-model", + "comment": "Add new ApiExportedMixin mixin class for determining whether an API item is exported or not", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor-model" +} \ No newline at end of file diff --git a/common/changes/@microsoft/api-extractor/exported_2022-07-26-16-12.json b/common/changes/@microsoft/api-extractor/exported_2022-07-26-16-12.json new file mode 100644 index 00000000000..2ff6210fef3 --- /dev/null +++ b/common/changes/@microsoft/api-extractor/exported_2022-07-26-16-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/api-extractor", + "comment": "Add new \"apiReport.includeForgottenExports\" and \"docModel.includeForgottenExports\" properties to control whether forgotten exports are included in the API report and doc model files.", + "type": "minor" + } + ], + "packageName": "@microsoft/api-extractor" +} \ No newline at end of file diff --git a/common/reviews/api/api-extractor-model.api.md b/common/reviews/api/api-extractor-model.api.md index c062e02597f..a9505bde992 100644 --- a/common/reviews/api/api-extractor-model.api.md +++ b/common/reviews/api/api-extractor-model.api.md @@ -172,6 +172,21 @@ export class ApiEnumMember extends ApiEnumMember_base { get kind(): ApiItemKind; } +// @public +export function ApiExportedMixin(baseClass: TBaseClass): TBaseClass & (new (...args: any[]) => ApiExportedMixin); + +// @public +export interface ApiExportedMixin extends ApiItem { + readonly isExported: boolean; + // @override (undocumented) + serializeInto(jsonObject: Partial): void; +} + +// @public +export namespace ApiExportedMixin { + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiExportedMixin; +} + // Warning: (ae-forgotten-export) The symbol "ApiFunction_base" needs to be exported by the entry point index.d.ts // // @public @@ -708,7 +723,7 @@ export interface IApiCallSignatureOptions extends IApiTypeParameterListMixinOpti } // @public -export interface IApiClassOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiTypeParameterListMixinOptions { +export interface IApiClassOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiTypeParameterListMixinOptions, IApiExportedMixinOptions { // (undocumented) extendsTokenRange: IExcerptTokenRange | undefined; // (undocumented) @@ -744,11 +759,17 @@ export interface IApiEnumMemberOptions extends IApiNameMixinOptions, IApiRelease } // @public -export interface IApiEnumOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions { +export interface IApiEnumOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiExportedMixinOptions { +} + +// @public +export interface IApiExportedMixinOptions extends IApiItemOptions { + // (undocumented) + isExported: boolean; } // @public -export interface IApiFunctionOptions extends IApiNameMixinOptions, IApiTypeParameterListMixinOptions, IApiParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiReturnTypeMixinOptions, IApiDeclaredItemOptions { +export interface IApiFunctionOptions extends IApiNameMixinOptions, IApiTypeParameterListMixinOptions, IApiParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiReturnTypeMixinOptions, IApiDeclaredItemOptions, IApiExportedMixinOptions { } // @public @@ -762,7 +783,7 @@ export interface IApiInitializerMixinOptions extends IApiItemOptions { } // @public -export interface IApiInterfaceOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiTypeParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions { +export interface IApiInterfaceOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiTypeParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiExportedMixinOptions { // (undocumented) extendsTokenRanges: IExcerptTokenRange[]; } @@ -798,7 +819,7 @@ export interface IApiNameMixinOptions extends IApiItemOptions { } // @public -export interface IApiNamespaceOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions { +export interface IApiNamespaceOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiExportedMixinOptions { } // @public @@ -883,7 +904,7 @@ export interface IApiStaticMixinOptions extends IApiItemOptions { } // @public -export interface IApiTypeAliasOptions extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiTypeParameterListMixinOptions { +export interface IApiTypeAliasOptions extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiTypeParameterListMixinOptions, IApiExportedMixinOptions { // (undocumented) typeTokenRange: IExcerptTokenRange; } @@ -905,7 +926,7 @@ export interface IApiTypeParameterOptions { } // @public -export interface IApiVariableOptions extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiReadonlyMixinOptions, IApiDeclaredItemOptions, IApiInitializerMixinOptions { +export interface IApiVariableOptions extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiReadonlyMixinOptions, IApiDeclaredItemOptions, IApiInitializerMixinOptions, IApiExportedMixinOptions { // (undocumented) variableTypeTokenRange: IExcerptTokenRange; } diff --git a/common/reviews/api/api-extractor.api.md b/common/reviews/api/api-extractor.api.md index 0ad47931415..c9706b61323 100644 --- a/common/reviews/api/api-extractor.api.md +++ b/common/reviews/api/api-extractor.api.md @@ -49,9 +49,11 @@ export class ExtractorConfig { readonly alphaTrimmedFilePath: string; readonly apiJsonFilePath: string; readonly apiReportEnabled: boolean; + readonly apiReportIncludeForgottenExports: boolean; readonly betaTrimmedFilePath: string; readonly bundledPackages: string[]; readonly docModelEnabled: boolean; + readonly docModelIncludeForgottenExports: boolean; readonly enumMemberOrder: EnumMemberOrder; static readonly FILENAME: string; getDiagnosticDump(): string; @@ -168,6 +170,7 @@ export interface ICompilerStateCreateOptions { // @public export interface IConfigApiReport { enabled: boolean; + includeForgottenExports?: boolean; reportFileName?: string; reportFolder?: string; reportTempFolder?: string; @@ -184,6 +187,7 @@ export interface IConfigCompiler { export interface IConfigDocModel { apiJsonFilePath?: string; enabled: boolean; + includeForgottenExports?: boolean; } // @public diff --git a/libraries/api-extractor-model/src/index.ts b/libraries/api-extractor-model/src/index.ts index 6b475af89d1..7e3bf3b9b9e 100644 --- a/libraries/api-extractor-model/src/index.ts +++ b/libraries/api-extractor-model/src/index.ts @@ -39,6 +39,7 @@ export { IApiNameMixinOptions, ApiNameMixin } from './mixins/ApiNameMixin'; export { IApiOptionalMixinOptions, ApiOptionalMixin } from './mixins/ApiOptionalMixin'; export { IApiReadonlyMixinOptions, ApiReadonlyMixin } from './mixins/ApiReadonlyMixin'; export { IApiInitializerMixinOptions, ApiInitializerMixin } from './mixins/ApiInitializerMixin'; +export { IApiExportedMixinOptions, ApiExportedMixin } from './mixins/ApiExportedMixin'; export { IFindApiItemsResult, IFindApiItemsMessage, diff --git a/libraries/api-extractor-model/src/mixins/ApiExportedMixin.ts b/libraries/api-extractor-model/src/mixins/ApiExportedMixin.ts new file mode 100644 index 00000000000..de37c3bb80c --- /dev/null +++ b/libraries/api-extractor-model/src/mixins/ApiExportedMixin.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information.s + +import { DeclarationReference, Navigation } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem'; +import { DeserializerContext } from '../model/DeserializerContext'; + +/** + * Constructor options for {@link (IApiExportedMixinOptions:interface)}. + * @public + */ +export interface IApiExportedMixinOptions extends IApiItemOptions { + isExported: boolean; +} + +export interface IApiExportedMixinJson extends IApiItemJson { + isExported: boolean; +} + +const _isExported: unique symbol = Symbol('ApiExportedMixin._isExported'); + +/** + * The mixin base class for API items that can be exported. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * + * @public + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface ApiExportedMixin extends ApiItem { + /** + * Whether the declaration is exported from its parent item container (i.e. either an `ApiEntryPoint` or an + * `ApiNamespace`). + * + * @remarks + * Suppose `index.ts` is your entry point: + * + * ```ts + * // index.ts + * + * export class A {} + * class B {} + * + * namespace n { + * export class C {} + * class D {} + * } + * + * // file.ts + * export class E {} + * ``` + * + * Classes `A` and `C` are both exported, while classes `B`, `D`, and `E` are not. `E` is exported from its + * local file, but not from its parent item container (i.e. the entry point). + * + */ + readonly isExported: boolean; + + /** @override */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiExportedMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiExportedMixin:interface)} functionality. + * + * @public + */ +export function ApiExportedMixin( + baseClass: TBaseClass + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): TBaseClass & (new (...args: any[]) => ApiExportedMixin) { + class MixedClass extends baseClass implements ApiExportedMixin { + public [_isExported]: boolean; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public constructor(...args: any[]) { + super(...args); + + const options: IApiExportedMixinOptions = args[0]; + this[_isExported] = options.isExported; + } + + /** @override */ + public static onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiExportedMixinJson + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + const declarationReference: DeclarationReference = DeclarationReference.parse( + jsonObject.canonicalReference + ); + options.isExported = declarationReference.navigation === Navigation.Exports; + } + + public get isExported(): boolean { + return this[_isExported]; + } + + /** + * The `isExported` property is intentionally not serialized because the information is already present + * in the item's `canonicalReference`. + * @override + */ + public serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiExportedMixin:interface)}. + * @public + */ +export namespace ApiExportedMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiExportedMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiExportedMixin { + return apiItem.hasOwnProperty(_isExported); + } +} diff --git a/libraries/api-extractor-model/src/model/ApiClass.ts b/libraries/api-extractor-model/src/model/ApiClass.ts index df6de4ac5bb..d207d96a6b9 100644 --- a/libraries/api-extractor-model/src/model/ApiClass.ts +++ b/libraries/api-extractor-model/src/model/ApiClass.ts @@ -20,6 +20,11 @@ import { IApiTypeParameterListMixinJson } from '../mixins/ApiTypeParameterListMixin'; import { DeserializerContext } from './DeserializerContext'; +import { + IApiExportedMixinJson, + IApiExportedMixinOptions, + ApiExportedMixin +} from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiClass}. @@ -30,12 +35,16 @@ export interface IApiClassOptions IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, - IApiTypeParameterListMixinOptions { + IApiTypeParameterListMixinOptions, + IApiExportedMixinOptions { extendsTokenRange: IExcerptTokenRange | undefined; implementsTokenRanges: IExcerptTokenRange[]; } -export interface IApiClassJson extends IApiDeclaredItemJson, IApiTypeParameterListMixinJson { +export interface IApiClassJson + extends IApiDeclaredItemJson, + IApiTypeParameterListMixinJson, + IApiExportedMixinJson { extendsTokenRange?: IExcerptTokenRange; implementsTokenRanges: IExcerptTokenRange[]; } @@ -57,7 +66,7 @@ export interface IApiClassJson extends IApiDeclaredItemJson, IApiTypeParameterLi * @public */ export class ApiClass extends ApiItemContainerMixin( - ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiDeclaredItem))) + ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem)))) ) { /** * The base class that this class inherits from (using the `extends` keyword), or undefined if there is no base class. @@ -128,8 +137,9 @@ export class ApiClass extends ApiItemContainerMixin( /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Class); } } diff --git a/libraries/api-extractor-model/src/model/ApiEnum.ts b/libraries/api-extractor-model/src/model/ApiEnum.ts index 14675678135..f58a6d91272 100644 --- a/libraries/api-extractor-model/src/model/ApiEnum.ts +++ b/libraries/api-extractor-model/src/model/ApiEnum.ts @@ -13,6 +13,7 @@ import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiRel import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin'; import { ApiEnumMember } from './ApiEnumMember'; import { IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin'; +import { IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiEnum}. @@ -22,7 +23,8 @@ export interface IApiEnumOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, - IApiDeclaredItemOptions {} + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} /** * Represents a TypeScript enum declaration. @@ -44,7 +46,9 @@ export interface IApiEnumOptions * * @public */ -export class ApiEnum extends ApiItemContainerMixin(ApiNameMixin(ApiReleaseTagMixin(ApiDeclaredItem))) { +export class ApiEnum extends ApiItemContainerMixin( + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))) +) { public constructor(options: IApiEnumOptions) { super(options); } @@ -79,8 +83,9 @@ export class ApiEnum extends ApiItemContainerMixin(ApiNameMixin(ApiReleaseTagMix /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Enum); } } diff --git a/libraries/api-extractor-model/src/model/ApiFunction.ts b/libraries/api-extractor-model/src/model/ApiFunction.ts index d74dc44e8a9..9bcba681ca4 100644 --- a/libraries/api-extractor-model/src/model/ApiFunction.ts +++ b/libraries/api-extractor-model/src/model/ApiFunction.ts @@ -17,6 +17,7 @@ import { IApiTypeParameterListMixinOptions, ApiTypeParameterListMixin } from '../mixins/ApiTypeParameterListMixin'; +import { IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiFunction}. @@ -28,7 +29,8 @@ export interface IApiFunctionOptions IApiParameterListMixinOptions, IApiReleaseTagMixinOptions, IApiReturnTypeMixinOptions, - IApiDeclaredItemOptions {} + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} /** * Represents a TypeScript function declaration. @@ -52,7 +54,9 @@ export interface IApiFunctionOptions * @public */ export class ApiFunction extends ApiNameMixin( - ApiTypeParameterListMixin(ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiDeclaredItem)))) + ApiTypeParameterListMixin( + ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiExportedMixin(ApiDeclaredItem)))) + ) ) { public constructor(options: IApiFunctionOptions) { super(options); @@ -75,8 +79,9 @@ export class ApiFunction extends ApiNameMixin( /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Function) .withOverloadIndex(this.overloadIndex); } diff --git a/libraries/api-extractor-model/src/model/ApiInterface.ts b/libraries/api-extractor-model/src/model/ApiInterface.ts index bef31906e2e..21bb42b806b 100644 --- a/libraries/api-extractor-model/src/model/ApiInterface.ts +++ b/libraries/api-extractor-model/src/model/ApiInterface.ts @@ -28,6 +28,11 @@ import { ApiTypeParameterListMixin } from '../mixins/ApiTypeParameterListMixin'; import { DeserializerContext } from './DeserializerContext'; +import { + IApiExportedMixinJson, + IApiExportedMixinOptions, + ApiExportedMixin +} from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiInterface}. @@ -38,7 +43,8 @@ export interface IApiInterfaceOptions IApiNameMixinOptions, IApiTypeParameterListMixinOptions, IApiReleaseTagMixinOptions, - IApiDeclaredItemOptions { + IApiDeclaredItemOptions, + IApiExportedMixinOptions { extendsTokenRanges: IExcerptTokenRange[]; } @@ -47,7 +53,8 @@ export interface IApiInterfaceJson IApiNameMixinJson, IApiTypeParameterListMixinJson, IApiReleaseTagMixinJson, - IApiDeclaredItemJson { + IApiDeclaredItemJson, + IApiExportedMixinJson { extendsTokenRanges: IExcerptTokenRange[]; } @@ -69,7 +76,7 @@ export interface IApiInterfaceJson * @public */ export class ApiInterface extends ApiItemContainerMixin( - ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiDeclaredItem))) + ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem)))) ) { private readonly _extendsTypes: HeritageType[] = []; @@ -123,8 +130,9 @@ export class ApiInterface extends ApiItemContainerMixin( /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Interface); } } diff --git a/libraries/api-extractor-model/src/model/ApiNamespace.ts b/libraries/api-extractor-model/src/model/ApiNamespace.ts index b19569c22b5..49281089332 100644 --- a/libraries/api-extractor-model/src/model/ApiNamespace.ts +++ b/libraries/api-extractor-model/src/model/ApiNamespace.ts @@ -12,6 +12,7 @@ import { ApiItemContainerMixin, IApiItemContainerMixinOptions } from '../mixins/ import { IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem'; import { ApiReleaseTagMixin, IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin'; import { IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin'; +import { IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiClass}. @@ -21,7 +22,8 @@ export interface IApiNamespaceOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions, IApiReleaseTagMixinOptions, - IApiDeclaredItemOptions {} + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} /** * Represents a TypeScript namespace declaration. @@ -45,7 +47,9 @@ export interface IApiNamespaceOptions * * @public */ -export class ApiNamespace extends ApiItemContainerMixin(ApiNameMixin(ApiReleaseTagMixin(ApiDeclaredItem))) { +export class ApiNamespace extends ApiItemContainerMixin( + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))) +) { public constructor(options: IApiNamespaceOptions) { super(options); } @@ -67,8 +71,9 @@ export class ApiNamespace extends ApiItemContainerMixin(ApiNameMixin(ApiReleaseT /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Namespace); } } diff --git a/libraries/api-extractor-model/src/model/ApiTypeAlias.ts b/libraries/api-extractor-model/src/model/ApiTypeAlias.ts index 759971b8cd3..71f018ef259 100644 --- a/libraries/api-extractor-model/src/model/ApiTypeAlias.ts +++ b/libraries/api-extractor-model/src/model/ApiTypeAlias.ts @@ -18,6 +18,11 @@ import { IApiTypeParameterListMixinJson } from '../mixins/ApiTypeParameterListMixin'; import { DeserializerContext } from './DeserializerContext'; +import { + IApiExportedMixinJson, + IApiExportedMixinOptions, + ApiExportedMixin +} from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiTypeAlias}. @@ -27,11 +32,15 @@ export interface IApiTypeAliasOptions extends IApiNameMixinOptions, IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, - IApiTypeParameterListMixinOptions { + IApiTypeParameterListMixinOptions, + IApiExportedMixinOptions { typeTokenRange: IExcerptTokenRange; } -export interface IApiTypeAliasJson extends IApiDeclaredItemJson, IApiTypeParameterListMixinJson { +export interface IApiTypeAliasJson + extends IApiDeclaredItemJson, + IApiTypeParameterListMixinJson, + IApiExportedMixinJson { typeTokenRange: IExcerptTokenRange; } @@ -62,7 +71,7 @@ export interface IApiTypeAliasJson extends IApiDeclaredItemJson, IApiTypeParamet * @public */ export class ApiTypeAlias extends ApiTypeParameterListMixin( - ApiNameMixin(ApiReleaseTagMixin(ApiDeclaredItem)) + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))) ) { /** * An {@link Excerpt} that describes the type of the alias. @@ -118,8 +127,9 @@ export class ApiTypeAlias extends ApiTypeParameterListMixin( /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.TypeAlias); } } diff --git a/libraries/api-extractor-model/src/model/ApiVariable.ts b/libraries/api-extractor-model/src/model/ApiVariable.ts index d7e55624496..a3ac4b9614e 100644 --- a/libraries/api-extractor-model/src/model/ApiVariable.ts +++ b/libraries/api-extractor-model/src/model/ApiVariable.ts @@ -15,6 +15,11 @@ import { IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin'; import { ApiInitializerMixin, IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin'; import { IExcerptTokenRange, Excerpt } from '../mixins/Excerpt'; import { DeserializerContext } from './DeserializerContext'; +import { + IApiExportedMixinJson, + IApiExportedMixinOptions, + ApiExportedMixin +} from '../mixins/ApiExportedMixin'; /** * Constructor options for {@link ApiVariable}. @@ -25,11 +30,12 @@ export interface IApiVariableOptions IApiReleaseTagMixinOptions, IApiReadonlyMixinOptions, IApiDeclaredItemOptions, - IApiInitializerMixinOptions { + IApiInitializerMixinOptions, + IApiExportedMixinOptions { variableTypeTokenRange: IExcerptTokenRange; } -export interface IApiVariableJson extends IApiDeclaredItemJson { +export interface IApiVariableJson extends IApiDeclaredItemJson, IApiExportedMixinJson { variableTypeTokenRange: IExcerptTokenRange; } @@ -54,7 +60,7 @@ export interface IApiVariableJson extends IApiDeclaredItemJson { * @public */ export class ApiVariable extends ApiNameMixin( - ApiReleaseTagMixin(ApiReadonlyMixin(ApiInitializerMixin(ApiDeclaredItem))) + ApiReleaseTagMixin(ApiReadonlyMixin(ApiInitializerMixin(ApiExportedMixin(ApiDeclaredItem)))) ) { /** * An {@link Excerpt} that describes the type of the variable. @@ -102,8 +108,9 @@ export class ApiVariable extends ApiNameMixin( /** @beta @override */ public buildCanonicalReference(): DeclarationReference { const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) - .addNavigationStep(Navigation.Exports, nameComponent) + .addNavigationStep(navigation, nameComponent) .withMeaning(Meaning.Variable); } }