feat(api-extractor): support multiple entrypoints (#10829)

* feat(api-extractor): support multiple entrypoints

* chore: initial support in generateSplitDocumentation

* chore: bring in line with upstream

* refactor: multiple entrypoints in scripts

* fix: split docs

* feat: website

* fix: docs failing on next

* fix: don't include dtypes for now

* refactor: don't fetch entrypoint if there is none

---------

Co-authored-by: iCrawl <buechler.noel@outlook.com>
This commit is contained in:
Qjuh
2025-05-12 23:48:41 +02:00
committed by GitHub
parent 4f5e5c7c14
commit b3db92edfb
93 changed files with 2330 additions and 1956 deletions

View File

@@ -51,6 +51,7 @@ import type { AstModule } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { TypeScriptInternals } from '../analyzer/TypeScriptInternals.js';
import type { ExtractorConfig } from '../api/ExtractorConfig';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
@@ -210,6 +211,16 @@ interface IProcessAstEntityContext {
parentDocgenJson?: DocgenContainerJson | undefined;
}
/**
* @beta
*/
export interface IApiModelGenerationOptions {
/**
* The release tags to trim.
*/
releaseTagsToTrim: Set<ReleaseTag>;
}
const linkRegEx =
/{@link\s(?:(?<class>\w+)(?:[#.](?<event>event:)?(?<prop>[\w()]+))?|(?<url>https?:\/\/[^\s}]*))(?<name>\s[^}]*)?}/g;
@@ -239,12 +250,25 @@ export class ApiModelGenerator {
private readonly _referenceGenerator: DeclarationReferenceGenerator;
private readonly _releaseTagsToTrim: Set<ReleaseTag> | undefined;
public readonly docModelEnabled: boolean;
private readonly _jsDocJson: DocgenJson | undefined;
public constructor(collector: Collector) {
public constructor(collector: Collector, extractorConfig: ExtractorConfig) {
this._collector = collector;
this._apiModel = new ApiModel();
this._referenceGenerator = new DeclarationReferenceGenerator(collector);
const apiModelGenerationOptions: IApiModelGenerationOptions | undefined = extractorConfig.docModelGenerationOptions;
if (apiModelGenerationOptions) {
this._releaseTagsToTrim = apiModelGenerationOptions.releaseTagsToTrim;
this.docModelEnabled = true;
} else {
this.docModelEnabled = false;
}
// @ts-expect-error we reuse the private tsdocParser from collector here
this._tsDocParser = collector._tsdocParser;
}
@@ -270,20 +294,22 @@ export class ApiModelGenerator {
});
this._apiModel.addMember(apiPackage);
const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: '' });
apiPackage.addMember(apiEntryPoint);
for (const [entryPoint, entities] of this._collector.entities.entries()) {
const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: entryPoint.modulePath });
apiPackage.addMember(apiEntryPoint);
for (const entity of this._collector.entities) {
// Only process entities that are exported from the entry point. Entities that are exported from
// `AstNamespaceImport` entities will be processed by `_processAstNamespaceImport`. However, if
// we are including forgotten exports, then process everything.
if (entity.exportedFromEntryPoint || this._collector.extractorConfig.docModelIncludeForgottenExports) {
this._processAstEntity(entity.astEntity, {
name: entity.nameForEmit!,
isExported: entity.exportedFromEntryPoint,
parentApiItem: apiEntryPoint,
parentDocgenJson: this._jsDocJson,
});
for (const entity of entities) {
// Only process entities that are exported from the entry point. Entities that are exported from
// `AstNamespaceImport` entities will be processed by `_processAstNamespaceImport`. However, if
// we are including forgotten exports, then process everything.
if (entity.exportedFromEntryPoint || this._collector.extractorConfig.docModelIncludeForgottenExports) {
this._processAstEntity(entity.astEntity, {
name: entity.nameForEmit!,
isExported: entity.exportedFromEntryPoint,
parentApiItem: apiEntryPoint,
parentDocgenJson: this._jsDocJson,
});
}
}
}
@@ -362,8 +388,8 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
if (releaseTag === ReleaseTag.Internal) {
return; // trim out items marked as "@internal"
if (this._releaseTagsToTrim?.has(releaseTag)) {
return;
}
switch (astDeclaration.declaration.kind) {
@@ -435,15 +461,31 @@ export class ApiModelGenerator {
this._processApiTypeAlias(astDeclaration, context);
break;
case ts.SyntaxKind.VariableDeclaration:
this._processApiVariable(astDeclaration, context);
case ts.SyntaxKind.VariableDeclaration: {
// check for arrow functions in variable declaration
const functionDeclaration: ts.FunctionDeclaration | undefined =
this._tryFindFunctionDeclaration(astDeclaration);
if (functionDeclaration) {
this._processApiFunction(astDeclaration, context, functionDeclaration);
} else {
this._processApiVariable(astDeclaration, context);
}
break;
}
default:
// ignore unknown types
}
}
private _tryFindFunctionDeclaration(astDeclaration: AstDeclaration): ts.FunctionDeclaration | undefined {
const children: readonly ts.Node[] = astDeclaration.declaration.getChildren(
astDeclaration.declaration.getSourceFile(),
);
return children.find(ts.isFunctionTypeNode) as ts.FunctionDeclaration | undefined;
}
private _processChildDeclarations(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
for (const childDeclaration of astDeclaration.children) {
this._processDeclaration(childDeclaration, {
@@ -817,7 +859,11 @@ export class ApiModelGenerator {
}
}
private _processApiFunction(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiFunction(
astDeclaration: AstDeclaration,
context: IProcessAstEntityContext,
altFunctionDeclaration?: ts.FunctionDeclaration,
): void {
const { name, isExported, parentApiItem } = context;
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
@@ -828,7 +874,8 @@ export class ApiModelGenerator {
const jsDoc = parent?.functions.find((fun) => fun.name === name);
if (apiFunction === undefined) {
const functionDeclaration: ts.FunctionDeclaration = astDeclaration.declaration as ts.FunctionDeclaration;
const functionDeclaration: ts.FunctionDeclaration =
altFunctionDeclaration ?? (astDeclaration.declaration as ts.FunctionDeclaration);
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
@@ -1799,11 +1846,20 @@ export class ApiModelGenerator {
.flatMap((typ, index) => {
const result = typ.reduce<IExcerptToken[]>((arr, [type, symbol]) => {
const astEntity =
(this._collector.entities.find(
(entity) => entity.nameForEmit === type && 'astDeclarations' in entity.astEntity,
)?.astEntity as AstSymbol | undefined) ??
(this._collector.entities.find((entity) => entity.nameForEmit === type && 'astSymbol' in entity.astEntity)
?.astEntity as AstImport | undefined);
[...this._collector.entities.values()].reduce<AstSymbol | undefined>(
(found, entities) =>
found ??
(entities.find((entity) => entity.nameForEmit === type && 'astDeclarations' in entity.astEntity)
?.astEntity as AstSymbol | undefined),
undefined,
) ??
[...this._collector.entities.values()].reduce<AstImport | undefined>(
(found, entities) =>
found ??
(entities.find((entity) => entity.nameForEmit === type && 'astSymbol' in entity.astEntity)
?.astEntity as AstImport | undefined),
undefined,
);
const astSymbol = astEntity instanceof AstImport ? astEntity.astSymbol : astEntity;
const match = astEntity instanceof AstImport ? moduleNameRegEx.exec(astEntity.modulePath) : null;
const pkg = match?.groups!.package ?? this._apiModel.packages[0]!.name;

View File

@@ -9,7 +9,7 @@ import * as ts from 'typescript';
import { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js';
@@ -17,12 +17,18 @@ import { Span } from '../analyzer/Span.js';
import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js';
import type { ExtractorMessage } from '../api/ExtractorMessage.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { ApiReportVariant } from '../api/IConfigFile';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import { Collector } from '../collector/Collector.js';
import type { CollectorEntity } from '../collector/CollectorEntity.js';
import type { SymbolMetadata } from '../collector/SymbolMetadata';
import { DtsEmitHelpers } from './DtsEmitHelpers.js';
import { IndentedWriter } from './IndentedWriter.js';
function capitalizeFirstLetter(input: string): string {
return input === '' ? '' : `${input[0]!.toLocaleUpperCase()}${input.slice(1)}`;
}
export class ApiReportGenerator {
private static _trimSpacesRegExp: RegExp = / +$/gm;
@@ -40,208 +46,235 @@ export class ApiReportGenerator {
return normalizedActual === normalizedExpected;
}
public static generateReviewFileContent(collector: Collector): string {
const writer: IndentedWriter = new IndentedWriter();
writer.trimLeadingSpaces = true;
/**
* Generates and returns the API report contents as a string.
*
* @param collector - The collector that has the entities.
* @param reportVariant - The release level with which the report is associated.
* Can also be viewed as the minimal release level of items that should be included in the report.
*/
public static generateReviewFileContent(collector: Collector, reportVariant: ApiReportVariant): Map<string, string> {
// mapping from entrypoint name to its file content
const fileContentMap: Map<string, string> = new Map<string, string>();
writer.writeLine(
[
`## API Report File for "${collector.workingPackage.name}"`,
``,
`> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`,
``,
].join('\n'),
);
for (const [entryPoint, entryPointEntities] of collector.entities) {
const writer: IndentedWriter = new IndentedWriter();
writer.trimLeadingSpaces = true;
// Write the opening delimiter for the Markdown code fence
writer.writeLine('```ts\n');
// For backwards compatibility, don't emit "complete" in report text for untrimmed reports.
const releaseLevelPrefix: string = reportVariant === 'complete' ? '' : `${capitalizeFirstLetter(reportVariant)} `;
writer.writeLine(
[
`## ${releaseLevelPrefix}API Report File for "${collector.workingPackage.name}${entryPoint.modulePath ? '/' : ''}${
entryPoint.modulePath
}"`,
``,
`> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`,
``,
].join('\n'),
);
// Emit the triple slash directives
for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) {
// https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162
writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`);
}
// Write the opening delimiter for the Markdown code fence
writer.writeLine('```ts\n');
for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) {
writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`);
}
writer.ensureSkippedLine();
// Emit the imports
for (const entity of collector.entities) {
if (entity.astEntity instanceof AstImport) {
DtsEmitHelpers.emitImport(writer, entity, entity.astEntity);
// Emit the triple slash directives
for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) {
// https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162
writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`);
}
}
writer.ensureSkippedLine();
for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) {
writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`);
}
// Emit the regular declarations
for (const entity of collector.entities) {
const astEntity: AstEntity = entity.astEntity;
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.
interface IExportToEmit {
readonly associatedMessages: ExtractorMessage[];
readonly exportName: string;
writer.ensureSkippedLine();
// Emit the imports
for (const entity of entryPointEntities) {
if (entity.astEntity instanceof AstImport) {
DtsEmitHelpers.emitImport(writer, entity, entity.astEntity);
}
const exportsToEmit: Map<string, IExportToEmit> = new Map<string, IExportToEmit>();
}
for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
writer.ensureSkippedLine();
// Emit the regular declarations
for (const entity of entryPointEntities) {
const astEntity: AstEntity = entity.astEntity;
const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity);
const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata?.maxEffectiveReleaseTag ?? ReleaseTag.None;
if (!this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, reportVariant)) {
continue;
}
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.
interface IExportToEmit {
readonly associatedMessages: ExtractorMessage[];
readonly exportName: string;
}
}
const exportsToEmit: Map<string, IExportToEmit> = new Map<string, IExportToEmit>();
if (astEntity instanceof AstSymbol) {
// Emit all the declarations for this entity
for (const astDeclaration of astEntity.astDeclarations || []) {
// Get the messages associated with this declaration
const fetchedMessages: ExtractorMessage[] =
collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration);
for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
}
}
// Peel off the messages associated with an export statement and store them
// in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
// added to messagesToReport, to be emitted next to the declaration instead of the export statement.
const messagesToReport: ExtractorMessage[] = [];
for (const message of fetchedMessages) {
if (message.properties.exportName) {
const exportToEmit: IExportToEmit | undefined = exportsToEmit.get(message.properties.exportName);
if (exportToEmit) {
exportToEmit.associatedMessages.push(message);
continue;
if (astEntity instanceof AstSymbol) {
// Emit all the declarations for this entity
for (const astDeclaration of astEntity.astDeclarations || []) {
// Get the messages associated with this declaration
const fetchedMessages: ExtractorMessage[] =
collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration);
// Peel off the messages associated with an export statement and store them
// in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
// added to messagesToReport, to be emitted next to the declaration instead of the export statement.
const messagesToReport: ExtractorMessage[] = [];
for (const message of fetchedMessages) {
if (message.properties.exportName) {
const exportToEmit: IExportToEmit | undefined = exportsToEmit.get(message.properties.exportName);
if (exportToEmit) {
exportToEmit.associatedMessages.push(message);
continue;
}
}
messagesToReport.push(message);
}
messagesToReport.push(message);
if (this._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) {
writer.ensureSkippedLine();
writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
const span: Span = new Span(astDeclaration.declaration);
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
if (apiItemMetadata.isPreapproved) {
ApiReportGenerator._modifySpanForPreapproved(span);
} else {
ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false, reportVariant);
}
span.writeModifiedText(writer);
writer.ensureNewLine();
}
}
writer.ensureSkippedLine();
writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
const span: Span = new Span(astDeclaration.declaration);
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
if (apiItemMetadata.isPreapproved) {
ApiReportGenerator._modifySpanForPreapproved(span);
} else {
ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false);
}
span.writeModifiedText(writer);
writer.ensureNewLine();
}
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
if (entity.nameForEmit === undefined) {
// This should never happen
throw new InternalError('referencedEntry.nameForEmit is undefined');
}
if (astModuleExportInfo.starExportedExternalModules.size > 0) {
// We could support this, but we would need to find a way to safely represent it.
throw new Error(
`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` +
SourceFileLocationFormatter.formatDeclaration(astEntity.declaration),
);
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
// Emit a synthetic declaration for the namespace. It will look like this:
//
// declare namespace example {
// export {
// f1,
// f2
// }
// }
//
// Note that we do not try to relocate f1()/f2() to be inside the namespace because other type
// signatures may reference them directly (without using the namespace qualifier).
writer.ensureSkippedLine();
writer.writeLine(`declare namespace ${entity.nameForEmit} {`);
// all local exports of local imported module are just references to top-level declarations
writer.increaseIndent();
writer.writeLine('export {');
writer.increaseIndent();
const exportClauses: string[] = [];
for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) {
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(exportedEntity);
if (collectorEntity === undefined) {
if (entity.nameForEmit === undefined) {
// This should never happen
// top-level exports of local imported module should be added as collector entities before
throw new InternalError(
`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`,
throw new InternalError('referencedEntry.nameForEmit is undefined');
}
if (astModuleExportInfo.starExportedExternalModules.size > 0) {
// We could support this, but we would need to find a way to safely represent it.
throw new Error(
`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` +
SourceFileLocationFormatter.formatDeclaration(astEntity.declaration),
);
}
if (collectorEntity.nameForEmit === exportedName) {
exportClauses.push(collectorEntity.nameForEmit);
} else {
exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`);
}
}
// Emit a synthetic declaration for the namespace. It will look like this:
//
// declare namespace example {
// export {
// f1,
// f2
// }
// }
//
// Note that we do not try to relocate f1()/f2() to be inside the namespace because other type
// signatures may reference them directly (without using the namespace qualifier).
writer.writeLine(exportClauses.join(',\n'));
writer.decreaseIndent();
writer.writeLine('}'); // end of "export { ... }"
writer.decreaseIndent();
writer.writeLine('}'); // end of "declare namespace { ... }"
}
// Now emit the export statements for this entity.
for (const exportToEmit of exportsToEmit.values()) {
// Write any associated messages
if (exportToEmit.associatedMessages.length > 0) {
writer.ensureSkippedLine();
for (const message of exportToEmit.associatedMessages) {
ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
writer.writeLine(`declare namespace ${entity.nameForEmit} {`);
// all local exports of local imported module are just references to top-level declarations
writer.increaseIndent();
writer.writeLine('export {');
writer.increaseIndent();
const exportClauses: string[] = [];
for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) {
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(exportedEntity);
if (collectorEntity === undefined) {
// This should never happen
// top-level exports of local imported module should be added as collector entities before
throw new InternalError(
`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`,
);
}
if (collectorEntity.nameForEmit === exportedName) {
exportClauses.push(collectorEntity.nameForEmit);
} else {
exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`);
}
}
writer.writeLine(exportClauses.join(',\n'));
writer.decreaseIndent();
writer.writeLine('}'); // end of "export { ... }"
writer.decreaseIndent();
writer.writeLine('}'); // end of "declare namespace { ... }"
}
DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity);
// Now emit the export statements for this entity.
for (const exportToEmit of exportsToEmit.values()) {
// Write any associated messages
if (exportToEmit.associatedMessages.length > 0) {
writer.ensureSkippedLine();
for (const message of exportToEmit.associatedMessages) {
ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
}
}
DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity);
}
writer.ensureSkippedLine();
}
}
DtsEmitHelpers.emitStarExports(writer, collector);
// Write the unassociated warnings at the bottom of the file
const unassociatedMessages: ExtractorMessage[] = collector.messageRouter.fetchUnassociatedMessagesForReviewFile();
if (unassociatedMessages.length > 0) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:');
ApiReportGenerator._writeLineAsComments(writer, '');
for (const unassociatedMessage of unassociatedMessages) {
ApiReportGenerator._writeLineAsComments(
writer,
unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder),
);
}
}
}
DtsEmitHelpers.emitStarExports(writer, collector);
// Write the unassociated warnings at the bottom of the file
const unassociatedMessages: ExtractorMessage[] = collector.messageRouter.fetchUnassociatedMessagesForReviewFile();
if (unassociatedMessages.length > 0) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:');
ApiReportGenerator._writeLineAsComments(writer, '');
for (const unassociatedMessage of unassociatedMessages) {
ApiReportGenerator._writeLineAsComments(
writer,
unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder),
);
if (collector.workingPackage.tsdocComment === undefined) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)');
}
}
if (collector.workingPackage.tsdocComment === undefined) {
// Write the closing delimiter for the Markdown code fence
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)');
writer.writeLine('```');
// Remove any trailing spaces
fileContentMap.set(entryPoint.modulePath, writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, ''));
}
// Write the closing delimiter for the Markdown code fence
writer.ensureSkippedLine();
writer.writeLine('```');
// Remove any trailing spaces
return writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, '');
return fileContentMap;
}
/**
@@ -253,10 +286,11 @@ export class ApiReportGenerator {
entity: CollectorEntity,
astDeclaration: AstDeclaration,
insideTypeLiteral: boolean,
reportVariant: ApiReportVariant,
): void {
// Should we process this declaration at all?
if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) {
if (!ApiReportGenerator._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) {
span.modification.skipAll();
return;
}
@@ -373,7 +407,14 @@ export class ApiReportGenerator {
case ts.SyntaxKind.ImportType:
DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => {
ApiReportGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, insideTypeLiteral);
ApiReportGenerator._modifySpan(
collector,
childSpan,
entity,
childAstDeclaration,
insideTypeLiteral,
reportVariant,
);
});
break;
@@ -408,11 +449,56 @@ export class ApiReportGenerator {
}
}
ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral);
ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral, reportVariant);
}
}
}
private static _shouldIncludeDeclaration(
collector: Collector,
astDeclaration: AstDeclaration,
reportVariant: ApiReportVariant,
): boolean {
// Private declarations are not included in the API report
if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) {
return false;
}
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
return this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, reportVariant);
}
private static _shouldIncludeReleaseTag(releaseTag: ReleaseTag, reportVariant: ApiReportVariant): boolean {
switch (reportVariant) {
case 'complete':
return true;
case 'alpha':
return (
releaseTag === ReleaseTag.Alpha ||
releaseTag === ReleaseTag.Beta ||
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
case 'beta':
return (
releaseTag === ReleaseTag.Beta ||
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
case 'public':
return (
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
default:
throw new Error(`Unrecognized release level: ${reportVariant}`);
}
}
/**
* For declarations marked as `@preapproved`, this is used instead of _modifySpan().
*/
@@ -482,30 +568,59 @@ export class ApiReportGenerator {
if (!collector.isAncillaryDeclaration(astDeclaration)) {
const footerParts: string[] = [];
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
// 1. Release tag (if present)
if (!apiItemMetadata.releaseTagSameAsParent && apiItemMetadata.effectiveReleaseTag !== ReleaseTag.None) {
footerParts.push(releaseTagGetTagName(apiItemMetadata.effectiveReleaseTag));
}
if (apiItemMetadata.isSealed) {
// 2. Enumerate configured tags, reporting standard system tags first and then other configured tags.
// Note that the ordering we handle the standard tags is important for backwards compatibility.
// Also note that we had special mechanisms for checking whether or not an item is documented with these tags,
// so they are checked specially.
const {
'@sealed': reportSealedTag,
'@virtual': reportVirtualTag,
'@override': reportOverrideTag,
'@eventProperty': reportEventPropertyTag,
'@deprecated': reportDeprecatedTag,
...otherTagsToReport
} = collector.extractorConfig.tagsToReport;
// 2.a Check for standard tags and report those that are both configured and present in the metadata.
if (reportSealedTag && apiItemMetadata.isSealed) {
footerParts.push('@sealed');
}
if (apiItemMetadata.isVirtual) {
if (reportVirtualTag && apiItemMetadata.isVirtual) {
footerParts.push('@virtual');
}
if (apiItemMetadata.isOverride) {
if (reportOverrideTag && apiItemMetadata.isOverride) {
footerParts.push('@override');
}
if (apiItemMetadata.isEventProperty) {
if (reportEventPropertyTag && apiItemMetadata.isEventProperty) {
footerParts.push('@eventProperty');
}
if (apiItemMetadata.tsdocComment?.deprecatedBlock) {
if (reportDeprecatedTag && apiItemMetadata.tsdocComment?.deprecatedBlock) {
footerParts.push('@deprecated');
}
// 2.b Check for other configured tags and report those that are present in the tsdoc metadata.
for (const [tag, reportTag] of Object.entries(otherTagsToReport)) {
// If the tag was not handled specially, check if it is present in the metadata.
if (
reportTag &&
(apiItemMetadata.tsdocComment?.customBlocks.some((block) => block.blockTag.tagName === tag) ||
apiItemMetadata.tsdocComment?.modifierTagSet.hasTagName(tag))
) {
footerParts.push(tag);
}
}
// 3. If the item is undocumented, append notice at the end of the list
if (apiItemMetadata.undocumented) {
footerParts.push('(undocumented)');

View File

@@ -341,7 +341,7 @@ export class DeclarationReferenceGenerator {
}
}
private _getPackageName(sourceFile: ts.SourceFile): string {
private _getEntryPointName(sourceFile: ts.SourceFile): string {
if (this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) {
const packageJson: INodePackageJson | undefined = this._collector.packageJsonLookup.tryLoadNodePackageJsonFor(
sourceFile.fileName,
@@ -354,18 +354,25 @@ export class DeclarationReferenceGenerator {
return DeclarationReferenceGenerator.unknownReference;
}
return this._collector.workingPackage.name;
let modulePath = '';
for (const entryPoint of this._collector.workingPackage.entryPoints) {
if (entryPoint.sourceFile === sourceFile) {
modulePath = entryPoint.modulePath;
}
}
return `${this._collector.workingPackage.name}${modulePath ? `/${modulePath}` : ''}`;
}
private _sourceFileToModuleSource(sourceFile: ts.SourceFile | undefined): GlobalSource | ModuleSource {
if (sourceFile && ts.isExternalModule(sourceFile)) {
const packageName: string = this._getPackageName(sourceFile);
const packageName: string = this._getEntryPointName(sourceFile);
if (this._collector.bundledPackageNames.has(packageName)) {
// The api-extractor.json config file has a "bundledPackages" setting, which causes imports from
// certain NPM packages to be treated as part of the working project. In this case, we need to
// substitute the working package name.
return new ModuleSource(this._collector.workingPackage.name);
return new ModuleSource(this._collector.workingPackage.name); // TODO: make it work with multiple entrypoints
} else {
return new ModuleSource(packageName);
}

View File

@@ -8,7 +8,7 @@ import * as ts from 'typescript';
import { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js';
@@ -103,8 +103,10 @@ export class DtsRollupGenerator {
writer.ensureSkippedLine();
// dtsRollup doesn't support multiple entry points. We throw error if dtsRollup is enabled while more than one entry points are specified.
// So at this point, we can safely assume there is only one entry point in collector.entities
// Emit the imports
for (const entity of collector.entities) {
for (const entity of [...collector.entities.values()][0]!) {
if (entity.astEntity instanceof AstImport) {
const astImport: AstImport = entity.astEntity;
@@ -124,7 +126,7 @@ export class DtsRollupGenerator {
writer.ensureSkippedLine();
// Emit the regular declarations
for (const entity of collector.entities) {
for (const entity of [...collector.entities.values()][0]!) {
const astEntity: AstEntity = entity.astEntity;
const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity);
const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata
@@ -159,7 +161,7 @@ export class DtsRollupGenerator {
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
if (entity.nameForEmit === undefined) {
// This should never happen

View File

@@ -1,119 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { IndentedWriter } from '../IndentedWriter.js';
test('01 Demo from docs', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.write('begin\n');
indentedWriter.increaseIndent();
indentedWriter.write('one\ntwo\n');
indentedWriter.decreaseIndent();
indentedWriter.increaseIndent();
indentedWriter.decreaseIndent();
indentedWriter.write('end');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('02 Indent something', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.write('a');
indentedWriter.write('b');
indentedWriter.increaseIndent();
indentedWriter.writeLine('c');
indentedWriter.writeLine('d');
indentedWriter.decreaseIndent();
indentedWriter.writeLine('e');
indentedWriter.increaseIndent('>>> ');
indentedWriter.writeLine();
indentedWriter.writeLine();
indentedWriter.writeLine('g');
indentedWriter.decreaseIndent();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('03 Indent something with indentBlankLines=true', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.indentBlankLines = true;
indentedWriter.write('a');
indentedWriter.write('b');
indentedWriter.increaseIndent();
indentedWriter.writeLine('c');
indentedWriter.writeLine('d');
indentedWriter.decreaseIndent();
indentedWriter.writeLine('e');
indentedWriter.increaseIndent('>>> ');
indentedWriter.writeLine();
indentedWriter.writeLine();
indentedWriter.writeLine('g');
indentedWriter.decreaseIndent();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('04 Two kinds of indents', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.writeLine('---');
indentedWriter.indentScope(() => {
indentedWriter.write('a\nb');
indentedWriter.indentScope(() => {
indentedWriter.write('c\nd\n');
});
indentedWriter.write('e\n');
}, '> ');
indentedWriter.writeLine('---');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('05 Edge cases for ensureNewLine()', () => {
let indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.ensureNewLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
indentedWriter = new IndentedWriter();
indentedWriter.write('previous');
indentedWriter.ensureNewLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('06 Edge cases for ensureSkippedLine()', () => {
let indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.ensureSkippedLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
indentedWriter = new IndentedWriter();
indentedWriter.write('previous');
indentedWriter.ensureSkippedLine();
indentedWriter.write('line');
indentedWriter.ensureSkippedLine();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('06 trimLeadingSpaces=true', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.trimLeadingSpaces = true;
// Example from doc comment
indentedWriter.increaseIndent(' ');
indentedWriter.write(' a\n b c\n');
indentedWriter.decreaseIndent();
indentedWriter.ensureSkippedLine();
indentedWriter.increaseIndent('>>');
indentedWriter.write(' ');
indentedWriter.write(' ');
indentedWriter.write(' a');
indentedWriter.writeLine(' b');
indentedWriter.writeLine('\ttab'); // does not get indented
indentedWriter.writeLine('c ');
expect(indentedWriter.toString()).toMatchSnapshot();
});

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`01 Demo from docs 1`] = `
"begin
one
two
end"
`;
exports[`02 Indent something 1`] = `
"abc
d
e
>>> g
"
`;
exports[`03 Indent something with indentBlankLines=true 1`] = `
"abc
d
e
>>>
>>>
>>> g
"
`;
exports[`04 Two kinds of indents 1`] = `
"---
> a
> bc
> d
> e
---
"
`;
exports[`05 Edge cases for ensureNewLine() 1`] = `"line"`;
exports[`05 Edge cases for ensureNewLine() 2`] = `
"previous
line"
`;
exports[`06 Edge cases for ensureSkippedLine() 1`] = `"line"`;
exports[`06 Edge cases for ensureSkippedLine() 2`] = `
"previous
line
"
`;
exports[`06 trimLeadingSpaces=true 1`] = `
" a
b c
>>a b
>> tab
>>c
"
`;