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

@@ -8,10 +8,10 @@ import type { AstSymbol } from './AstSymbol.js';
/**
* Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo}
*/
export class AstModuleExportInfo {
public readonly exportedLocalEntities: Map<string, AstEntity> = new Map<string, AstEntity>();
public readonly starExportedExternalModules: Set<AstModule> = new Set<AstModule>();
export interface IAstModuleExportInfo {
readonly exportedLocalEntities: Map<string, AstEntity>;
readonly starExportedExternalModules: Set<AstModule>;
readonly visitedAstModules: Set<AstModule>;
}
/**
@@ -64,7 +64,7 @@ export class AstModule {
/**
* Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`.
*/
public astModuleExportInfo: AstModuleExportInfo | undefined;
public astModuleExportInfo: IAstModuleExportInfo | undefined;
public constructor(options: IAstModuleOptions) {
this.sourceFile = options.sourceFile;

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { AstNamespaceImport, type IAstNamespaceImportOptions } from './AstNamespaceImport';
export interface IAstNamespaceExportOptions extends IAstNamespaceImportOptions {}
/**
* `AstNamespaceExport` represents a namespace that is created implicitly and exported by a statement
* such as `export * as example from "./file";`
*
* @remarks
*
* A typical input looks like this:
* ```ts
* // Suppose that example.ts exports two functions f1() and f2().
* export * as example from "./file";
* ```
*
* API Extractor's .d.ts rollup will transform it into an explicit namespace, like this:
* ```ts
* declare f1(): void;
* declare f2(): void;
*
* export declare namespace example {
* export {
* f1,
* f2
* }
* }
* ```
*
* The current implementation does not attempt to relocate f1()/f2() to be inside the `namespace`
* because other type signatures may reference them directly (without using the namespace qualifier).
* The AstNamespaceExports behaves the same as AstNamespaceImport, it just also has the inline export for the craeted namespace.
*/
export class AstNamespaceExport extends AstNamespaceImport {
public constructor(options: IAstNamespaceExportOptions) {
super(options);
}
}

View File

@@ -4,7 +4,7 @@
import type * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import { AstSyntheticEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
import type { AstModule, IAstModuleExportInfo } from './AstModule.js';
export interface IAstNamespaceImportOptions {
readonly astModule: AstModule;
@@ -88,8 +88,8 @@ export class AstNamespaceImport extends AstSyntheticEntity {
return this.namespaceName;
}
public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule);
public fetchAstModuleExportInfo(collector: Collector): IAstModuleExportInfo {
const astModuleExportInfo: IAstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule);
return astModuleExportInfo;
}
}

View File

@@ -5,7 +5,7 @@ import * as tsdoc from '@microsoft/tsdoc';
import * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
import type { WorkingPackage } from '../collector/WorkingPackage.js';
import type { IWorkingPackageEntryPoint, WorkingPackage } from '../collector/WorkingPackage.js';
import type { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule } from './AstModule.js';
@@ -52,7 +52,10 @@ export class AstReferenceResolver {
this._workingPackage = collector.workingPackage;
}
public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure {
public resolve(
declarationReference: tsdoc.DocDeclarationReference,
entryPoint: IWorkingPackageEntryPoint,
): AstDeclaration | ResolverFailure {
// Is it referring to the working package?
if (
declarationReference.packageName !== undefined &&
@@ -66,9 +69,7 @@ export class AstReferenceResolver {
return new ResolverFailure('Import paths are not supported');
}
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(
this._workingPackage.entryPointSourceFile,
);
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(entryPoint.sourceFile);
if (declarationReference.memberReferences.length === 0) {
return new ResolverFailure('Package references are not supported');

View File

@@ -8,7 +8,7 @@ import * as ts from 'typescript';
import type { MessageRouter } from '../collector/MessageRouter';
import { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
import type { AstModule, IAstModuleExportInfo } from './AstModule.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import { ExportAnalyzer } from './ExportAnalyzer.js';
@@ -126,7 +126,7 @@ export class AstSymbolTable {
/**
* This crawls the specified entry point and collects the full set of exported AstSymbols.
*/
public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(astModule: AstModule): IAstModuleExportInfo {
return this._exportAnalyzer.fetchAstModuleExportInfo(astModule);
}

View File

@@ -5,7 +5,8 @@ import { InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { AstEntity } from './AstEntity.js';
import { AstImport, type IAstImportOptions, AstImportKind } from './AstImport.js';
import { AstModule, AstModuleExportInfo } from './AstModule.js';
import { AstModule, type IAstModuleExportInfo } from './AstModule.js';
import { AstNamespaceExport } from './AstNamespaceExport.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import type { IFetchAstSymbolOptions } from './AstSymbolTable.js';
@@ -226,15 +227,19 @@ export class ExportAnalyzer {
/**
* Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}.
*/
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): IAstModuleExportInfo {
if (entryPointAstModule.isExternal) {
throw new Error('fetchAstModuleExportInfo() is not supported for external modules');
}
if (entryPointAstModule.astModuleExportInfo === undefined) {
const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo();
const astModuleExportInfo: IAstModuleExportInfo = {
visitedAstModules: new Set<AstModule>(),
exportedLocalEntities: new Map<string, AstEntity>(),
starExportedExternalModules: new Set<AstModule>(),
};
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set<AstModule>());
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule);
entryPointAstModule.astModuleExportInfo = astModuleExportInfo;
}
@@ -255,11 +260,7 @@ export class ExportAnalyzer {
: importOrExportDeclaration.moduleSpecifier;
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
specifier && ts.isStringLiteralLike(specifier)
? ts.getModeForUsageLocation(
importOrExportDeclaration.getSourceFile(),
specifier,
this._program.getCompilerOptions(),
)
? this._program.getModeForUsageLocation(importOrExportDeclaration.getSourceFile(), specifier)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
@@ -304,11 +305,8 @@ export class ExportAnalyzer {
return this._importableAmbientSourceFiles.has(sourceFile);
}
private _collectAllExportsRecursive(
astModuleExportInfo: AstModuleExportInfo,
astModule: AstModule,
visitedAstModules: Set<AstModule>,
): void {
private _collectAllExportsRecursive(astModuleExportInfo: IAstModuleExportInfo, astModule: AstModule): void {
const { visitedAstModules, starExportedExternalModules, exportedLocalEntities } = astModuleExportInfo;
if (visitedAstModules.has(astModule)) {
return;
}
@@ -316,7 +314,7 @@ export class ExportAnalyzer {
visitedAstModules.add(astModule);
if (astModule.isExternal) {
astModuleExportInfo.starExportedExternalModules.add(astModule);
starExportedExternalModules.add(astModule);
} else {
// Fetch each of the explicit exports for this module
if (astModule.moduleSymbol.exports) {
@@ -329,7 +327,7 @@ export class ExportAnalyzer {
// Don't collect the "export default" symbol unless this is the entry point module
if (
(exportName !== ts.InternalSymbolName.Default || visitedAstModules.size === 1) &&
!astModuleExportInfo.exportedLocalEntities.has(exportSymbol.name)
!exportedLocalEntities.has(exportSymbol.name)
) {
const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule);
@@ -341,7 +339,7 @@ export class ExportAnalyzer {
this._astSymbolTable.analyze(astEntity);
}
astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity);
exportedLocalEntities.set(exportSymbol.name, astEntity);
}
break;
@@ -350,7 +348,7 @@ export class ExportAnalyzer {
}
for (const starExportedModule of astModule.starExportedModules) {
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules);
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule);
}
}
}
@@ -550,8 +548,8 @@ export class ExportAnalyzer {
// SemicolonToken: pre=[;]
// Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780
const namespaceExport: ts.NamespaceExport = declaration as ts.NamespaceExport;
exportName = namespaceExport.name.getText().trim();
const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
return this._getAstNamespaceExport(astModule, declarationSymbol, declaration);
// throw new Error(
// `The "export * as ___" syntax is not supported yet; as a workaround,` +
// ` use "import * as ___" with a separate "export { ___ }" declaration\n` +
@@ -568,32 +566,32 @@ export class ExportAnalyzer {
if (exportDeclaration.moduleSpecifier) {
const externalModulePath: string | undefined = this._tryGetExternalModulePath(exportDeclaration);
if (declaration.kind === ts.SyntaxKind.NamespaceExport) {
if (externalModulePath === undefined) {
const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
// if (declaration.kind === ts.SyntaxKind.NamespaceExport) {
// if (externalModulePath === undefined) {
// const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
// let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
// if (namespaceImport === undefined) {
// namespaceImport = new AstNamespaceImport({
// namespaceName: declarationSymbol.name,
// astModule,
// declaration,
// symbol: declarationSymbol,
// });
// this._astNamespaceImportByModule.set(astModule, namespaceImport);
// }
return namespaceImport;
}
// return namespaceImport;
// }
// Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
// a package or source file.
return this._fetchAstImport(undefined, {
importKind: AstImportKind.StarImport,
exportName,
modulePath: externalModulePath,
isTypeOnly: exportDeclaration.isTypeOnly,
});
}
// // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
// // a package or source file.
// return this._fetchAstImport(undefined, {
// importKind: AstImportKind.StarImport,
// exportName,
// modulePath: externalModulePath,
// isTypeOnly: exportDeclaration.isTypeOnly,
// });
// }
if (externalModulePath !== undefined) {
return this._fetchAstImport(declarationSymbol, {
@@ -611,6 +609,21 @@ export class ExportAnalyzer {
return undefined;
}
private _getAstNamespaceExport(
astModule: AstModule,
declarationSymbol: ts.Symbol,
declaration: ts.Declaration,
): AstNamespaceExport {
const imoprtNamespace: AstNamespaceImport = this._getAstNamespaceImport(astModule, declarationSymbol, declaration);
return new AstNamespaceExport({
namespaceName: imoprtNamespace.localName,
astModule,
declaration,
symbol: declarationSymbol,
});
}
private _tryMatchImportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined {
const importDeclaration: ts.ImportDeclaration | undefined = TypeScriptHelpers.findFirstParent<ts.ImportDeclaration>(
declaration,
@@ -637,18 +650,7 @@ export class ExportAnalyzer {
if (externalModulePath === undefined) {
const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol);
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
return namespaceImport;
return this._getAstNamespaceImport(astModule, declarationSymbol, declaration);
}
// Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
@@ -770,6 +772,25 @@ export class ExportAnalyzer {
return undefined;
}
private _getAstNamespaceImport(
astModule: AstModule,
declarationSymbol: ts.Symbol,
declaration: ts.Declaration,
): AstNamespaceImport {
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
return namespaceImport;
}
private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean {
if (importDeclaration.importClause) {
return Boolean(importDeclaration.importClause.isTypeOnly);
@@ -883,10 +904,9 @@ export class ExportAnalyzer {
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
importOrExportDeclaration.moduleSpecifier && ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier)
? ts.getModeForUsageLocation(
? this._program.getModeForUsageLocation(
importOrExportDeclaration.getSourceFile(),
importOrExportDeclaration.moduleSpecifier,
this._program.getCompilerOptions(),
)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(

View File

@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
/* eslint-disable sonarjs/no-nested-switch */
import path from 'node:path';
import {
type PackageJsonLookup,
FileSystem,
@@ -9,7 +9,9 @@ import {
type NewlineKind,
type INodePackageJson,
type JsonObject,
type IPackageJsonExports,
} from '@rushstack/node-core-library';
import semver from 'semver';
import { ConsoleMessageId } from '../api/ConsoleMessageId.js';
import { Extractor } from '../api/Extractor.js';
import type { MessageRouter } from '../collector/MessageRouter.js';
@@ -43,6 +45,136 @@ export class PackageMetadata {
}
}
const TSDOC_METADATA_FILENAME = 'tsdoc-metadata.json' as const;
/**
* 1. If package.json a `"tsdocMetadata": "./path1/path2/tsdoc-metadata.json"` field
* then that takes precedence. This convention will be rarely needed, since the other rules below generally
* produce a good result.
*/
function _tryResolveTsdocMetadataFromTsdocMetadataField({ tsdocMetadata }: INodePackageJson): string | undefined {
return tsdocMetadata;
}
/**
* 2. If package.json contains a `"exports": { ".": { "types": "./path1/path2/index.d.ts" } }` field,
* then we look for the file under "./path1/path2/tsdoc-metadata.json"
*
* This always looks for a "." and then a "*" entry in the exports field, and then evaluates for
* a "types" field in that entry.
*/
function _tryResolveTsdocMetadataFromExportsField({ exports }: INodePackageJson): string | undefined {
switch (typeof exports) {
case 'string': {
return `${path.dirname(exports)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
if (Array.isArray(exports)) {
const [firstExport] = exports;
// Take the first entry in the array
if (firstExport) {
return `${path.dirname(firstExport)}/${TSDOC_METADATA_FILENAME}`;
}
} else {
const rootExport: IPackageJsonExports | string | null | undefined = exports['.'] ?? exports['*'];
switch (typeof rootExport) {
case 'string': {
return `${path.dirname(rootExport)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
let typesExport: IPackageJsonExports | string | undefined = rootExport?.types;
while (typesExport) {
switch (typeof typesExport) {
case 'string': {
return `${path.dirname(typesExport)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
typesExport = typesExport?.types;
break;
}
}
}
}
}
}
break;
}
}
return undefined;
}
/**
* 3. If package.json contains a `typesVersions` field, look for the version
* matching the highest minimum version that either includes a "." or "*" entry.
*/
function _tryResolveTsdocMetadataFromTypesVersionsField({ typesVersions }: INodePackageJson): string | undefined {
if (typesVersions) {
let highestMinimumMatchingSemver: semver.SemVer | undefined;
let latestMatchingPath: string | undefined;
for (const [version, paths] of Object.entries(typesVersions)) {
let range: semver.Range;
try {
range = new semver.Range(version);
} catch {
continue;
}
const minimumMatchingSemver: semver.SemVer | null = semver.minVersion(range);
if (
minimumMatchingSemver &&
(!highestMinimumMatchingSemver || semver.gt(minimumMatchingSemver, highestMinimumMatchingSemver))
) {
const pathEntry: string[] | undefined = paths['.'] ?? paths['*'];
const firstPath: string | undefined = pathEntry?.[0];
if (firstPath) {
highestMinimumMatchingSemver = minimumMatchingSemver;
latestMatchingPath = firstPath;
}
}
}
if (latestMatchingPath) {
return `${path.dirname(latestMatchingPath)}/${TSDOC_METADATA_FILENAME}`;
}
}
return undefined;
}
/**
* 4. If package.json contains a `"types": "./path1/path2/index.d.ts"` or a `"typings": "./path1/path2/index.d.ts"`
* field, then we look for the file under "./path1/path2/tsdoc-metadata.json".
*
* @remarks
* `types` takes precedence over `typings`.
*/
function _tryResolveTsdocMetadataFromTypesOrTypingsFields({ typings, types }: INodePackageJson): string | undefined {
const typesField: string | undefined = types ?? typings;
if (typesField) {
return `${path.dirname(typesField)}/${TSDOC_METADATA_FILENAME}`;
}
return undefined;
}
/**
* 5. If package.json contains a `"main": "./path1/path2/index.js"` field, then we look for the file under
* "./path1/path2/tsdoc-metadata.json".
*/
function _tryResolveTsdocMetadataFromMainField({ main }: INodePackageJson): string | undefined {
if (main) {
return `${path.dirname(main)}/${TSDOC_METADATA_FILENAME}`;
}
return undefined;
}
/**
* This class maintains a cache of analyzed information obtained from package.json
* files. It is built on top of the PackageJsonLookup class.
@@ -57,7 +189,7 @@ export class PackageMetadata {
* Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager.
*/
export class PackageMetadataManager {
public static tsdocMetadataFilename: string = 'tsdoc-metadata.json';
public static tsdocMetadataFilename: string = TSDOC_METADATA_FILENAME;
private readonly _packageJsonLookup: PackageJsonLookup;
@@ -70,37 +202,30 @@ export class PackageMetadataManager {
this._messageRouter = messageRouter;
}
// This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
/**
* This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
* In the future we will use the \@microsoft/tsdoc library to read this file.
*/
private static _resolveTsdocMetadataPathFromPackageJson(
packageFolder: string,
packageJson: INodePackageJson,
): string {
const tsdocMetadataFilename: string = PackageMetadataManager.tsdocMetadataFilename;
let tsdocMetadataRelativePath: string;
if (packageJson.tsdocMetadata) {
// 1. If package.json contains a field such as "tsdocMetadata": "./path1/path2/tsdoc-metadata.json",
// then that takes precedence. This convention will be rarely needed, since the other rules below generally
// produce a good result.
tsdocMetadataRelativePath = packageJson.tsdocMetadata;
} else if (packageJson.typings) {
// 2. If package.json contains a field such as "typings": "./path1/path2/index.d.ts", then we look
// for the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.typings), tsdocMetadataFilename);
} else if (packageJson.main) {
// 3. If package.json contains a field such as "main": "./path1/path2/index.js", then we look for
// the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.main), tsdocMetadataFilename);
} else {
// 4. If none of the above rules apply, then by default we look for the file under "./tsdoc-metadata.json"
// since the default entry point is "./index.js"
tsdocMetadataRelativePath = tsdocMetadataFilename;
}
const tsdocMetadataRelativePath: string =
_tryResolveTsdocMetadataFromTsdocMetadataField(packageJson) ??
_tryResolveTsdocMetadataFromExportsField(packageJson) ??
_tryResolveTsdocMetadataFromTypesVersionsField(packageJson) ??
_tryResolveTsdocMetadataFromTypesOrTypingsFields(packageJson) ??
_tryResolveTsdocMetadataFromMainField(packageJson) ??
// As a final fallback, place the file in the root of the package.
TSDOC_METADATA_FILENAME;
// Always resolve relative to the package folder.
const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath);
const tsdocMetadataPath: string = path.resolve(
packageFolder,
// This non-null assertion is safe because the last entry in TSDOC_METADATA_RESOLUTION_FUNCTIONS
// returns a non-undefined value.
tsdocMetadataRelativePath!,
);
return tsdocMetadataPath;
}

View File

@@ -2,7 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError, Sort } from '@rushstack/node-core-library';
import { InternalError, Sort, Text } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import { IndentedWriter } from '../generators/IndentedWriter.js';
@@ -664,13 +664,7 @@ export class Span {
}
private _getTrimmed(text: string): string {
const trimmed: string = text.replaceAll(/\r?\n/g, '\\n');
if (trimmed.length > 100) {
return trimmed.slice(0, 97) + '...';
}
return trimmed;
return Text.truncateWithEllipsis(Text.convertToLf(text), 100);
}
private _getSubstring(startIndex: number, endIndex: number): string {

View File

@@ -12,12 +12,6 @@ export interface IGlobalVariableAnalyzer {
}
export class TypeScriptInternals {
public static getImmediateAliasedSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol {
// Compiler internal:
// https://github.com/microsoft/TypeScript/blob/v3.2.2/src/compiler/checker.ts
return (typeChecker as any).getImmediateAliasedSymbol(symbol);
}
/**
* Returns the Symbol for the provided Declaration. This is a workaround for a missing
* feature of the TypeScript Compiler API. It is the only apparent way to reach
@@ -90,19 +84,6 @@ export class TypeScriptInternals {
return result?.resolvedModule;
}
/**
* Gets the mode required for module resolution required with the addition of Node16/nodenext
*/
public static getModeForUsageLocation(
file: { impliedNodeFormat?: ts.SourceFile['impliedNodeFormat'] },
usage: ts.StringLiteralLike | undefined,
): ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined {
// Compiler internal:
// https://github.com/microsoft/TypeScript/blob/v4.7.2/src/compiler/program.ts#L568
return (ts as any).getModeForUsageLocation?.(file, usage);
}
/**
* Returns ts.Symbol.parent if it exists.
*/

View File

@@ -1,123 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { FileSystem, PackageJsonLookup, type INodePackageJson, NewlineKind } from '@rushstack/node-core-library';
import { PackageMetadataManager } from '../PackageMetadataManager.js';
const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup();
function resolveInTestPackage(testPackageName: string, ...args: string[]): string {
return path.resolve(__dirname, 'test-data/tsdoc-metadata-path-inference', testPackageName, ...args);
}
function getPackageMetadata(testPackageName: string): {
packageFolder: string;
packageJson: INodePackageJson;
} {
const packageFolder: string = resolveInTestPackage(testPackageName);
const packageJson: INodePackageJson | undefined = packageJsonLookup.tryLoadPackageJsonFor(packageFolder);
if (!packageJson) {
throw new Error('There should be a package.json file in the test package');
}
return { packageFolder, packageJson };
}
function firstArgument(mockFn: jest.Mock): any {
return mockFn.mock.calls[0][0];
}
describe(PackageMetadataManager.name, () => {
describe(PackageMetadataManager.writeTsdocMetadataFile.name, () => {
const originalWriteFile = FileSystem.writeFile;
const mockWriteFile: jest.Mock = jest.fn();
beforeAll(() => {
FileSystem.writeFile = mockWriteFile;
});
afterEach(() => {
mockWriteFile.mockClear();
});
afterAll(() => {
FileSystem.writeFile = originalWriteFile;
});
it('writes the tsdoc metadata file at the provided path', () => {
PackageMetadataManager.writeTsdocMetadataFile('/foo/bar', NewlineKind.CrLf);
expect(firstArgument(mockWriteFile)).toBe('/foo/bar');
});
});
describe(PackageMetadataManager.resolveTsdocMetadataPath.name, () => {
describe('when an empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath = '';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata path as given by "tsdocMetadata" relative to the folder of package.json', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdoc-metadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, packageJson.tsdocMetadata as string),
);
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "typings"', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, path.dirname(packageJson.typings!), 'tsdoc-metadata.json'),
);
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "main"', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, path.dirname(packageJson.main!), 'tsdoc-metadata.json'),
);
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, 'tsdoc-metadata.json'),
);
});
});
});
describe('when a non-empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath = 'path/to/custom-tsdoc-metadata.json';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdocMetadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
});
});
});

View File

@@ -1,57 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { SyntaxHelpers } from '../SyntaxHelpers.js';
describe(SyntaxHelpers.name, () => {
it(SyntaxHelpers.makeCamelCaseIdentifier.name, () => {
// prettier-ignore
const inputs:string[] = [
'',
'@#(&*^',
'api-extractor-lib1-test',
'one',
'one-two',
'ONE-TWO',
'ONE/two/ /three/FOUR',
'01234'
];
expect(inputs.map((x) => ({ input: x, output: SyntaxHelpers.makeCamelCaseIdentifier(x) }))).toMatchInlineSnapshot(`
Array [
Object {
"input": "",
"output": "_",
},
Object {
"input": "@#(&*^",
"output": "_",
},
Object {
"input": "api-extractor-lib1-test",
"output": "apiExtractorLib1Test",
},
Object {
"input": "one",
"output": "one",
},
Object {
"input": "one-two",
"output": "oneTwo",
},
Object {
"input": "ONE-TWO",
"output": "oneTwo",
},
Object {
"input": "ONE/two/ /three/FOUR",
"output": "oneTwoThreeFour",
},
Object {
"input": "01234",
"output": "_01234",
},
]
`);
});
});

View File

@@ -1,4 +0,0 @@
{
"name": "package-default",
"version": "1.0.0"
}

View File

@@ -1,5 +0,0 @@
{
"name": "package-inferred-from-main",
"version": "1.0.0",
"main": "path/to/main.js"
}

View File

@@ -1,7 +0,0 @@
{
"name": "package-inferred-from-tsdoc-metadata",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts",
"tsdocMetadata": "path/to/tsdoc-metadata.json"
}

View File

@@ -1,6 +0,0 @@
{
"name": "package-inferred-from-typings",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts"
}