mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-12 09:33:32 +01:00
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:
@@ -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;
|
||||
|
||||
42
packages/api-extractor/src/analyzer/AstNamespaceExport.ts
Normal file
42
packages/api-extractor/src/analyzer/AstNamespaceExport.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "package-default",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "package-inferred-from-main",
|
||||
"version": "1.0.0",
|
||||
"main": "path/to/main.js"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "package-inferred-from-typings",
|
||||
"version": "1.0.0",
|
||||
"main": "path/to/main.js",
|
||||
"typings": "path/to/typings.d.ts"
|
||||
}
|
||||
Reference in New Issue
Block a user