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

@@ -9,7 +9,7 @@ import { PackageDocComment } from '../aedoc/PackageDocComment.js';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModule, AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { AstModule, IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstReferenceResolver } from '../analyzer/AstReferenceResolver.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
@@ -18,12 +18,14 @@ import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js';
import { TypeScriptInternals, type IGlobalVariableAnalyzer } from '../analyzer/TypeScriptInternals.js';
import { ExtractorConfig } from '../api/ExtractorConfig.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { IConfigEntryPoint } from '../api/IConfigFile';
import { ApiItemMetadata, type IApiItemMetadataOptions } from './ApiItemMetadata.js';
import { CollectorEntity } from './CollectorEntity.js';
import { type DeclarationMetadata, InternalDeclarationMetadata } from './DeclarationMetadata.js';
import type { MessageRouter } from './MessageRouter.js';
import type { SourceMapper } from './SourceMapper.js';
import { SymbolMetadata } from './SymbolMetadata.js';
import type { IWorkingPackageEntryPoint } from './WorkingPackage.js';
import { WorkingPackage } from './WorkingPackage.js';
/**
@@ -83,12 +85,15 @@ export class Collector {
private readonly _tsdocParser: tsdoc.TSDocParser;
private _astEntryPoint: AstModule | undefined;
private readonly _entities: CollectorEntity[] = [];
private _astEntryPoints: AstModule[] | undefined;
private readonly _entitiesByAstEntity: Map<AstEntity, CollectorEntity> = new Map<AstEntity, CollectorEntity>();
private readonly _entitiesByAstEntryPoint: Map<IWorkingPackageEntryPoint, CollectorEntity[]> = new Map<
IWorkingPackageEntryPoint,
CollectorEntity[]
>();
private readonly _entitiesBySymbol: Map<ts.Symbol, CollectorEntity> = new Map<ts.Symbol, CollectorEntity>();
private readonly _starExportedExternalModulePaths: string[] = [];
@@ -107,13 +112,23 @@ export class Collector {
this.extractorConfig = options.extractorConfig;
this.sourceMapper = options.sourceMapper;
const entryPointSourceFile: ts.SourceFile | undefined = options.program.getSourceFile(
const entryPoints: IConfigEntryPoint[] = [
this.extractorConfig.mainEntryPointFilePath,
);
...this.extractorConfig.additionalEntryPoints,
];
if (!entryPointSourceFile) {
throw new Error('Unable to load file: ' + this.extractorConfig.mainEntryPointFilePath);
}
const workingPackageEntryPoints: IWorkingPackageEntryPoint[] = entryPoints.map((entryPoint) => {
const sourceFile: ts.SourceFile | undefined = options.program.getSourceFile(entryPoint.filePath);
if (!sourceFile) {
throw new Error('Unable to load file: ' + entryPoint.filePath);
}
return {
modulePath: entryPoint.modulePath,
sourceFile,
};
});
if (!this.extractorConfig.packageFolder || !this.extractorConfig.packageJson) {
throw new Error('Unable to find a package.json file for the project being analyzed');
@@ -122,7 +137,7 @@ export class Collector {
this.workingPackage = new WorkingPackage({
packageFolder: this.extractorConfig.packageFolder,
packageJson: this.extractorConfig.packageJson,
entryPointSourceFile,
entryPoints: workingPackageEntryPoints,
});
this.messageRouter = options.messageRouter;
@@ -169,8 +184,8 @@ export class Collector {
return this._dtsLibReferenceDirectives;
}
public get entities(): readonly CollectorEntity[] {
return this._entities;
public get entities(): ReadonlyMap<IWorkingPackageEntryPoint, CollectorEntity[]> {
return this._entitiesByAstEntryPoint;
}
/**
@@ -185,7 +200,7 @@ export class Collector {
* Perform the analysis.
*/
public analyze(): void {
if (this._astEntryPoint) {
if (this._astEntryPoints) {
throw new Error('DtsRollupGenerator.analyze() was already called');
}
@@ -230,59 +245,101 @@ export class Collector {
);
}
// Build the entry point
const entryPointSourceFile: ts.SourceFile = this.workingPackage.entryPointSourceFile;
// Build entry points
for (const entryPoint of this.workingPackage.entryPoints) {
const { sourceFile: entryPointSourceFile } = entryPoint;
const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile);
this._astEntryPoint = astEntryPoint;
const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile);
const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile(
entryPointSourceFile,
this,
);
if (this._astEntryPoints) {
this._astEntryPoints.push(astEntryPoint);
} else {
this._astEntryPoints = [astEntryPoint];
}
if (packageDocCommentTextRange) {
const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
entryPointSourceFile.text,
packageDocCommentTextRange.pos,
packageDocCommentTextRange.end,
);
if (!this._entitiesByAstEntryPoint.has(entryPoint)) {
this._entitiesByAstEntryPoint.set(entryPoint, []);
}
this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range);
// Process pacakgeDocComment only for the default entry point
if (this.workingPackage.isDefaultEntryPoint(entryPoint)) {
const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile(
entryPointSourceFile,
this,
);
this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile);
if (packageDocCommentTextRange) {
const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
entryPointSourceFile.text,
packageDocCommentTextRange.pos,
packageDocCommentTextRange.end,
);
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}
this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range);
const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);
this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile);
// Create a CollectorEntity for each top-level export.
const processedAstEntities: AstEntity[] = [];
for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) {
this._createCollectorEntity(astEntity, exportName);
processedAstEntities.push(astEntity);
}
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}
}
// Recursively create the remaining CollectorEntities after the top-level entities
// have been processed.
const alreadySeenAstEntities: Set<AstEntity> = new Set<AstEntity>();
for (const astEntity of processedAstEntities) {
this._recursivelyCreateEntities(astEntity, alreadySeenAstEntities);
if (astEntity instanceof AstSymbol) {
this.fetchSymbolMetadata(astEntity);
const { exportedLocalEntities, starExportedExternalModules, visitedAstModules }: IAstModuleExportInfo =
this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);
// Create a CollectorEntity for each top-level export.
const processedAstEntities: AstEntity[] = [];
for (const [exportName, astEntity] of exportedLocalEntities) {
this._createCollectorEntity(astEntity, entryPoint, exportName);
processedAstEntities.push(astEntity);
}
// Recursively create the remaining CollectorEntities after the top-level entities
// have been processed.
const alreadySeenAstEntities: Set<AstEntity> = new Set<AstEntity>();
for (const astEntity of processedAstEntities) {
this._recursivelyCreateEntities(astEntity, entryPoint, alreadySeenAstEntities);
if (astEntity instanceof AstSymbol) {
this.fetchSymbolMetadata(astEntity);
}
}
// Ensure references are collected from any intermediate files that
// only include exports
const nonExternalSourceFiles: Set<ts.SourceFile> = new Set();
for (const { sourceFile, isExternal } of visitedAstModules) {
if (!nonExternalSourceFiles.has(sourceFile) && !isExternal) {
nonExternalSourceFiles.add(sourceFile);
}
}
// Here, we're collecting reference directives from all non-external source files
// that were encountered while looking for exports, but only those references that
// were explicitly written by the developer and marked with the `preserve="true"`
// attribute. In TS >= 5.5, only references that are explicitly authored and marked
// with `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The `_collectReferenceDirectives` function pulls in all references in files that
// contain definitions, but does not examine files that only reexport from other
// files. Here, we're looking through files that were missed by `_collectReferenceDirectives`,
// but only collecting references that were explicitly marked with `preserve="true"`.
// It is intuitive for developers to include references that they explicitly want part of
// their public API in a file like the entrypoint, which is likely to only contain reexports,
// and this picks those up.
this._collectReferenceDirectivesFromSourceFiles(nonExternalSourceFiles, true);
this._makeUniqueNames(entryPoint);
for (const starExportedExternalModule of starExportedExternalModules) {
if (starExportedExternalModule.externalModulePath !== undefined) {
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
}
}
}
this._makeUniqueNames();
for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) {
if (starExportedExternalModule.externalModulePath !== undefined) {
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
}
for (const entities of this._entitiesByAstEntryPoint.values()) {
Sort.sortBy(entities, (x) => x.getSortKey());
}
Sort.sortBy(this._entities, (x) => x.getSortKey());
Sort.sortSet(this._dtsTypeReferenceDirectives);
Sort.sortSet(this._dtsLibReferenceDirectives);
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
@@ -433,7 +490,12 @@ export class Collector {
return overloadIndex;
}
private _createCollectorEntity(astEntity: AstEntity, exportName?: string, parent?: CollectorEntity): CollectorEntity {
private _createCollectorEntity(
astEntity: AstEntity,
entryPoint: IWorkingPackageEntryPoint,
exportName?: string | undefined,
parent?: CollectorEntity,
): void {
let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
if (!entity) {
@@ -446,10 +508,16 @@ export class Collector {
this._entitiesBySymbol.set(astEntity.symbol, entity);
}
this._entities.push(entity);
this._collectReferenceDirectives(astEntity);
}
// add collectorEntity to the corresponding entry point
const entitiesOfAstModule: CollectorEntity[] = this._entitiesByAstEntryPoint.get(entryPoint) || [];
if (!entitiesOfAstModule.includes(entity)) {
entitiesOfAstModule.push(entity);
this._entitiesByAstEntryPoint.set(entryPoint, entitiesOfAstModule);
}
if (exportName) {
if (parent) {
entity.addLocalExportName(exportName, parent);
@@ -457,11 +525,13 @@ export class Collector {
entity.addExportName(exportName);
}
}
return entity;
}
private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set<AstEntity>): void {
private _recursivelyCreateEntities(
astEntity: AstEntity,
entryPoint: IWorkingPackageEntryPoint,
alreadySeenAstEntities: Set<AstEntity>,
): void {
if (alreadySeenAstEntities.has(astEntity)) return;
alreadySeenAstEntities.add(astEntity);
@@ -473,19 +543,19 @@ export class Collector {
// 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);
this._createCollectorEntity(referencedAstEntity, entryPoint);
}
} else {
this._createCollectorEntity(referencedAstEntity);
this._createCollectorEntity(referencedAstEntity, entryPoint);
}
this._recursivelyCreateEntities(referencedAstEntity, alreadySeenAstEntities);
this._recursivelyCreateEntities(referencedAstEntity, entryPoint, alreadySeenAstEntities);
}
});
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
const astModuleExportInfo: IAstModuleExportInfo = 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.
@@ -496,16 +566,16 @@ export class Collector {
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);
this._createCollectorEntity(localAstEntity, entryPoint, localExportName, parentEntity);
this._recursivelyCreateEntities(localAstEntity, entryPoint, alreadySeenAstEntities);
}
}
}
/**
* Ensures a unique name for each item in the package typings file.
* Ensures a unique name for each item in the entry point typings file.
*/
private _makeUniqueNames(): void {
private _makeUniqueNames(entryPoint: IWorkingPackageEntryPoint): void {
// The following examples illustrate the nameForEmit heuristics:
//
// Example 1:
@@ -527,8 +597,10 @@ export class Collector {
// Set of names that should NOT be used when generating a unique nameForEmit
const usedNames: Set<string> = new Set<string>();
const entities: CollectorEntity[] = this._entitiesByAstEntryPoint.get(entryPoint) || [];
// First collect the names of explicit package exports, and perform a sanity check.
for (const entity of this._entities) {
for (const entity of entities) {
for (const exportName of entity.exportNames) {
if (usedNames.has(exportName)) {
// This should be impossible
@@ -540,7 +612,7 @@ export class Collector {
}
// Ensure that each entity has a unique nameForEmit
for (const entity of this._entities) {
for (const entity of entities) {
// What name would we ideally want to emit it as?
let idealNameForEmit: string;
@@ -917,37 +989,80 @@ export class Collector {
}
private _collectReferenceDirectives(astEntity: AstEntity): void {
// Here, we're collecting reference directives from source files that contain extracted
// definitions (i.e. - files that contain `export class ...`, `export interface ...`, ...).
// These references may or may not include the `preserve="true" attribute. In TS < 5.5,
// references that end up in .D.TS files may or may not be explicity written by the developer.
// In TS >= 5.5, only references that are explicitly authored and are marked with
// `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The calls to `_collectReferenceDirectivesFromSourceFiles` in this function are
// preserving existing behavior, which is to include all reference directives
// regardless of whether they are explicitly authored or not, but only in files that
// contain definitions.
if (astEntity instanceof AstSymbol) {
const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) =>
astDeclaration.declaration.getSourceFile(),
);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
return;
}
if (astEntity instanceof AstNamespaceImport) {
const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile];
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
}
}
private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void {
private _collectReferenceDirectivesFromSourceFiles(
sourceFiles: Iterable<ts.SourceFile>,
onlyIncludeExplicitlyPreserved: boolean,
): void {
const seenFilenames: Set<string> = new Set<string>();
for (const sourceFile of sourceFiles) {
if (sourceFile?.fileName && !seenFilenames.has(sourceFile.fileName)) {
seenFilenames.add(sourceFile.fileName);
if (sourceFile?.fileName) {
const { fileName, typeReferenceDirectives, libReferenceDirectives, text: sourceFileText } = sourceFile;
if (!seenFilenames.has(fileName)) {
seenFilenames.add(fileName);
for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) {
const name: string = sourceFile.text.slice(typeReferenceDirective.pos, typeReferenceDirective.end);
this._dtsTypeReferenceDirectives.add(name);
}
for (const typeReferenceDirective of typeReferenceDirectives) {
const name: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
typeReferenceDirective,
onlyIncludeExplicitlyPreserved,
);
if (name) {
this._dtsTypeReferenceDirectives.add(name);
}
}
for (const libReferenceDirective of sourceFile.libReferenceDirectives) {
const name: string = sourceFile.text.slice(libReferenceDirective.pos, libReferenceDirective.end);
this._dtsLibReferenceDirectives.add(name);
for (const libReferenceDirective of libReferenceDirectives) {
const reference: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
libReferenceDirective,
onlyIncludeExplicitlyPreserved,
);
if (reference) {
this._dtsLibReferenceDirectives.add(reference);
}
}
}
}
}
}
private _getReferenceDirectiveFromSourceFile(
sourceFileText: string,
{ pos, end, preserve }: ts.FileReference,
onlyIncludeExplicitlyPreserved: boolean,
): string | undefined {
const reference: string = sourceFileText.slice(pos, end);
if (preserve || !onlyIncludeExplicitlyPreserved) {
return reference;
}
return undefined;
}
}

View File

@@ -9,11 +9,16 @@ import type * as ts from 'typescript';
* Constructor options for WorkingPackage
*/
export interface IWorkingPackageOptions {
entryPointSourceFile: ts.SourceFile;
entryPoints: IWorkingPackageEntryPoint[];
packageFolder: string;
packageJson: INodePackageJson;
}
export interface IWorkingPackageEntryPoint {
modulePath: string;
sourceFile: ts.SourceFile;
}
/**
* Information about the working package for a particular invocation of API Extractor.
*
@@ -51,7 +56,7 @@ export class WorkingPackage {
* only processes a single entry point during an invocation. This will be improved
* in the future.
*/
public readonly entryPointSourceFile: ts.SourceFile;
public readonly entryPoints: IWorkingPackageEntryPoint[];
/**
* The `@packageDocumentation` comment, if any, for the working package.
@@ -66,7 +71,7 @@ export class WorkingPackage {
public constructor(options: IWorkingPackageOptions) {
this.packageFolder = options.packageFolder;
this.packageJson = options.packageJson;
this.entryPointSourceFile = options.entryPointSourceFile;
this.entryPoints = options.entryPoints;
}
/**
@@ -75,4 +80,8 @@ export class WorkingPackage {
public get name(): string {
return this.packageJson.name;
}
public isDefaultEntryPoint(entryPoint: IWorkingPackageEntryPoint): boolean {
return entryPoint.modulePath === '';
}
}