feat: mainlib docs on new website (#9930)

* fix(ExceptText): don't display import("d..-types/v10"). in return type

* Squashed 'packages/api-extractor-model/' content from commit 39ecb196c

git-subtree-dir: packages/api-extractor-model
git-subtree-split: 39ecb196ca210bdf84ba6c9cadb1bb93571849d7

* Squashed 'packages/api-extractor/' content from commit 341ad6c51

git-subtree-dir: packages/api-extractor
git-subtree-split: 341ad6c51b01656d4f73b74ad4bdb3095f9262c4

* feat(api-extractor): add api-extractor and -model

* fix: package.json docs script

* fix(SourcLink): use <> instead of function syntax

* fix: make packages private

* fix: rest params showing in docs, added labels

* fix: missed two files

* feat: merge docs.json from docgen and docs.api.json

* fix: cpy-cli & pnpm-lock

* fix: increase icon size

* fix: icon size again

* feat: run both docs on mainlib

* chore: website fixes

* fix: more website fixes

* fix: tests and dev database script

* chore: comment out old docs

* fix: increase max fetch cache

* fix: env should always be a string

* fix: try to reapply patches

* fix: remove prepare for docgen

* fix: temporary cosmetic fixes

* fix: horizontal scroll

* feat: generate index for new docs

---------

Co-authored-by: Noel <buechler.noel@outlook.com>
This commit is contained in:
Qjuh
2023-11-08 10:16:54 +01:00
committed by GitHub
parent f713e47b0a
commit da455bceea
63 changed files with 1877 additions and 253 deletions

View File

@@ -131,7 +131,7 @@ export class PackageMetadataManager {
tsdocVersion: '0.12',
toolPackages: [
{
packageName: '@microsoft/api-extractor',
packageName: '@discordjs/api-extractor',
packageVersion: Extractor.version,
},
],

View File

@@ -121,5 +121,3 @@ describe(PackageMetadataManager.name, () => {
});
});
});
/* eslint-enable @typescript-eslint/typedef */

View File

@@ -142,7 +142,7 @@ export interface IExtractorConfigPrepareOptions {
/**
* Allow customization of the tsdoc.json config file. If omitted, this file will be loaded from its default
* location. If the file does not exist, then the standard definitions will be used from
* `@microsoft/api-extractor/extends/tsdoc-base.json`.
* `@discordjs/api-extractor/extends/tsdoc-base.json`.
*/
tsdocConfigFile?: TSDocConfigFile;
}

View File

@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import {
ApiModel,
ApiClass,
ApiPackage,
ApiEntryPoint,
ApiEvent,
ApiMethod,
ApiNamespace,
ApiInterface,
@@ -19,6 +21,7 @@ import {
ApiEnum,
ApiEnumMember,
type IExcerptTokenRange,
type IExcerptTokenRangeWithTypeParameters,
type IExcerptToken,
ApiConstructor,
ApiConstructSignature,
@@ -29,12 +32,17 @@ import {
ApiCallSignature,
type IApiTypeParameterOptions,
EnumMemberOrder,
ExcerptTokenKind,
Navigation,
} from '@discordjs/api-extractor-model';
import type * as tsdoc from '@microsoft/tsdoc';
import { Path } from '@rushstack/node-core-library';
import { TSDocParser } from '@microsoft/tsdoc';
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { JsonFile, Path } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import type { AstImport } from '../analyzer/AstImport.js';
import type { AstModule } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
@@ -46,10 +54,165 @@ import type { ISourceLocation } from '../collector/SourceMapper.js';
import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator.js';
import { ExcerptBuilder, type IExcerptBuilderNodeToCapture } from './ExcerptBuilder.js';
type DocgenAccess = 'private' | 'protected' | 'public';
type DocgenScope = 'global' | 'instance' | 'static';
type DocgenDeprecated = boolean | string;
interface DocgenMetaJson {
file: string;
line: number;
path: string;
}
interface DocgenTypeJson {
names?: string[] | undefined;
}
interface DocgenVarJson {
description?: string;
nullable?: boolean;
types?: string[][][];
}
type DocgenVarTypeJson = DocgenVarJson | string[][][];
interface DocgenExceptionJson {
description?: string;
nullable?: boolean;
type: DocgenTypeJson;
}
interface DocgenExternalJson {
description: string;
meta: DocgenMetaJson;
name: string;
see?: string[];
}
interface DocgenTypedefJson {
access?: DocgenAccess;
deprecated?: DocgenDeprecated;
description: string;
meta: DocgenMetaJson;
name: string;
params?: DocgenParamJson[];
props?: DocgenParamJson[];
returns?: DocgenVarTypeJson[];
see?: string[];
type: DocgenVarTypeJson;
}
interface DocgenEventJson {
deprecated?: DocgenDeprecated;
description: string;
meta: DocgenMetaJson;
name: string;
params?: DocgenParamJson[];
see?: string[];
}
interface DocgenParamJson {
default?: string;
description: string;
name: string;
nullable?: boolean;
optional?: boolean;
type: DocgenVarTypeJson;
variable?: string;
}
interface DocgenConstructorJson {
access?: DocgenAccess;
description: string;
name: string;
params?: DocgenParamJson[];
see?: string[];
}
interface DocgenMethodJson {
abstract: boolean;
access?: DocgenAccess;
async?: boolean;
deprecated?: DocgenDeprecated;
description: string;
emits?: string[];
examples?: string[];
generator?: boolean;
implements?: string[];
inherited?: boolean;
inherits?: string;
meta: DocgenMetaJson;
name: string;
params?: DocgenParamJson[];
returns?: DocgenVarTypeJson[];
scope: DocgenScope;
see?: string[];
throws?: DocgenExceptionJson[];
}
interface DocgenPropertyJson {
abstract?: boolean;
access?: DocgenAccess;
default?: string;
deprecated?: DocgenDeprecated;
description: string;
meta: DocgenMetaJson;
name: string;
nullable?: boolean;
props?: DocgenPropertyJson[];
readonly?: boolean;
scope: DocgenScope;
see?: string[];
type: DocgenVarTypeJson;
}
interface DocgenClassJson {
abstract?: boolean;
access?: DocgenAccess;
construct: DocgenConstructorJson;
deprecated?: DocgenDeprecated | string;
description: string;
events?: DocgenEventJson[];
extends?: DocgenVarTypeJson;
implements?: DocgenVarTypeJson;
meta: DocgenMetaJson;
methods?: DocgenMethodJson[];
name: string;
props?: DocgenPropertyJson[];
see?: string[];
}
type DocgenInterfaceJson = DocgenClassJson;
type DocgenContainerJson =
| DocgenClassJson
| DocgenConstructorJson
| DocgenInterfaceJson
| DocgenJson
| DocgenMethodJson
| DocgenTypedefJson;
export interface DocgenJson {
classes: DocgenClassJson[];
externals: DocgenExternalJson[];
functions: DocgenMethodJson[];
interfaces: DocgenInterfaceJson[];
meta: {
date: number;
format: number;
generator: string;
};
typedefs: DocgenTypedefJson[];
}
interface IProcessAstEntityContext {
isExported: boolean;
name: string;
parentApiItem: ApiItemContainerMixin;
parentDocgenJson?: DocgenContainerJson | undefined;
}
const linkRegEx = /{@link\s(?<class>\w+)#(?<event>event:)?(?<prop>[\w()]+)(?<name>\s[^}]*)?}/g;
function fixLinkTags(input?: string): string | undefined {
return input?.replaceAll(linkRegEx, '{@link $<class>.$<prop>$<name>}');
}
function filePathFromJson(meta: DocgenMetaJson): string {
return `${meta.path.slice('packages/discord.js/'.length)}/${meta.file}`;
}
export class ApiModelGenerator {
@@ -71,6 +234,12 @@ export class ApiModelGenerator {
public buildApiPackage(): ApiPackage {
const packageDocComment: tsdoc.DocComment | undefined = this._collector.workingPackage.tsdocComment;
let jsDocJson: DocgenJson | undefined;
const jsDocFilepath = `${this._collector.extractorConfig.apiJsonFilePath.slice(0, -8)}json`;
if (existsSync(jsDocFilepath)) {
jsDocJson = JsonFile.load(jsDocFilepath);
}
const apiPackage: ApiPackage = new ApiPackage({
name: this._collector.workingPackage.name,
@@ -92,6 +261,7 @@ export class ApiModelGenerator {
name: entity.nameForEmit!,
isExported: entity.exportedFromEntryPoint,
parentApiItem: apiEntryPoint,
parentDocgenJson: jsDocJson,
});
}
}
@@ -320,6 +490,7 @@ export class ApiModelGenerator {
const constructorDeclaration: ts.ConstructorDeclaration = astDeclaration.declaration as ts.ConstructorDeclaration;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const parameters: IApiParameterOptions[] = this._captureParameters(
nodesToCapture,
@@ -328,7 +499,15 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = parent?.construct
? new TSDocParser().parseString(
`/*+\n * ${fixLinkTags(parent.construct.description)}\n${
parent.construct.params
?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`)
.join('') ?? ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0;
const sourceLocation: ISourceLocation = this._getSourceLocation(constructorDeclaration);
@@ -340,8 +519,8 @@ export class ApiModelGenerator {
parameters,
overloadIndex,
excerptTokens,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: parent ? filePathFromJson(parent.meta) : sourceLocation.sourceFilePath,
fileLine: parent?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -354,9 +533,14 @@ export class ApiModelGenerator {
const containerKey: string = ApiClass.getContainerKey(name);
let apiClass: ApiClass | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiClass;
const parent = context.parentDocgenJson as DocgenJson | undefined;
const jsDoc = parent?.classes.find((_class) => _class.name === name);
if (apiClass === undefined) {
const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration;
if (name === 'ActionRow') {
console.dir(classDeclaration.heritageClauses?.[0]?.types[0]?.typeArguments, { depth: 3 });
}
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
@@ -365,18 +549,29 @@ export class ApiModelGenerator {
classDeclaration.typeParameters,
);
let extendsTokenRange: IExcerptTokenRange | undefined;
const implementsTokenRanges: IExcerptTokenRange[] = [];
let extendsTokenRange: IExcerptTokenRangeWithTypeParameters | undefined;
const implementsTokenRanges: IExcerptTokenRangeWithTypeParameters[] = [];
for (const heritageClause of classDeclaration.heritageClauses ?? []) {
if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
extendsTokenRange = ExcerptBuilder.createEmptyTokenRange();
extendsTokenRange = ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
if (heritageClause.types.length > 0) {
extendsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
) ?? []),
);
nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange });
}
} else if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) {
for (const heritageType of heritageClause.types) {
const implementsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
const implementsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
implementsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
) ?? []),
);
implementsTokenRanges.push(implementsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange });
}
@@ -385,7 +580,17 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${
jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isAbstract: boolean = (ts.getCombinedModifierFlags(classDeclaration) & ts.ModifierFlags.Abstract) !== 0;
const sourceLocation: ISourceLocation = this._getSourceLocation(classDeclaration);
@@ -400,8 +605,8 @@ export class ApiModelGenerator {
extendsTokenRange,
implementsTokenRanges,
isExported,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -411,7 +616,12 @@ export class ApiModelGenerator {
this._processChildDeclarations(astDeclaration, {
...context,
parentApiItem: apiClass,
parentDocgenJson: jsDoc,
});
for (const event of jsDoc?.events ?? []) {
this._processApiEvent({ ...context, name: event.name, parentApiItem: apiClass, parentDocgenJson: jsDoc });
}
}
private _processApiConstructSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
@@ -428,6 +638,7 @@ export class ApiModelGenerator {
astDeclaration.declaration as ts.ConstructSignatureDeclaration;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: constructSignature.type, tokenRange: returnTypeTokenRange });
@@ -441,7 +652,15 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = parent?.construct
? new TSDocParser().parseString(
`/*+\n * ${fixLinkTags(parent.construct.description)}\n${
parent.construct.params
?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`)
.join('') ?? ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const sourceLocation: ISourceLocation = this._getSourceLocation(constructSignature);
@@ -453,8 +672,8 @@ export class ApiModelGenerator {
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: parent ? filePathFromJson(parent.meta) : sourceLocation.sourceFilePath,
fileLine: parent?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -541,6 +760,8 @@ export class ApiModelGenerator {
const containerKey: string = ApiFunction.getContainerKey(name, overloadIndex);
let apiFunction: ApiFunction | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiFunction;
const parent = context.parentDocgenJson as DocgenJson | undefined;
const jsDoc = parent?.functions.find((fun) => fun.name === name);
if (apiFunction === undefined) {
const functionDeclaration: ts.FunctionDeclaration = astDeclaration.declaration as ts.FunctionDeclaration;
@@ -562,7 +783,24 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ??
''
}${
jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0])
? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}`
: ''
}${
jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const sourceLocation: ISourceLocation = this._getSourceLocation(functionDeclaration);
@@ -576,8 +814,8 @@ export class ApiModelGenerator {
excerptTokens,
returnTypeTokenRange,
isExported,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -633,6 +871,9 @@ export class ApiModelGenerator {
const containerKey: string = ApiInterface.getContainerKey(name);
let apiInterface: ApiInterface | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiInterface;
const parent = context.parentDocgenJson as DocgenJson | undefined;
const jsDoc =
parent?.interfaces.find((int) => int.name === name) ?? parent?.typedefs.find((int) => int.name === name);
if (apiInterface === undefined) {
const interfaceDeclaration: ts.InterfaceDeclaration = astDeclaration.declaration as ts.InterfaceDeclaration;
@@ -644,12 +885,18 @@ export class ApiModelGenerator {
interfaceDeclaration.typeParameters,
);
const extendsTokenRanges: IExcerptTokenRange[] = [];
const extendsTokenRanges: IExcerptTokenRangeWithTypeParameters[] = [];
for (const heritageClause of interfaceDeclaration.heritageClauses ?? []) {
if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) {
for (const heritageType of heritageClause.types) {
const extendsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
const extendsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
extendsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
) ?? []),
);
extendsTokenRanges.push(extendsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange });
}
@@ -658,7 +905,17 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${
jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const sourceLocation: ISourceLocation = this._getSourceLocation(interfaceDeclaration);
@@ -670,8 +927,8 @@ export class ApiModelGenerator {
typeParameters,
extendsTokenRanges,
isExported,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -681,6 +938,7 @@ export class ApiModelGenerator {
this._processChildDeclarations(astDeclaration, {
...context,
parentApiItem: apiInterface,
parentDocgenJson: jsDoc,
});
}
@@ -691,6 +949,8 @@ export class ApiModelGenerator {
const containerKey: string = ApiMethod.getContainerKey(name, isStatic, overloadIndex);
let apiMethod: ApiMethod | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiMethod;
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const jsDoc = parent?.methods?.find((method) => method.name === name);
if (apiMethod === undefined) {
const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration;
@@ -709,7 +969,24 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ??
''
}${
jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0])
? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}`
: ''
}${
jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) {
return; // trim out items marked as "@internal" or "@alpha"
@@ -733,8 +1010,8 @@ export class ApiModelGenerator {
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -750,6 +1027,8 @@ export class ApiModelGenerator {
let apiMethodSignature: ApiMethodSignature | undefined = parentApiItem.tryGetMemberByKey(
containerKey,
) as ApiMethodSignature;
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const jsDoc = parent?.methods?.find((method) => method.name === name);
if (apiMethodSignature === undefined) {
const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature;
@@ -768,7 +1047,24 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ??
''
}${
jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0])
? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}`
: ''
}${
jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const sourceLocation: ISourceLocation = this._getSourceLocation(methodSignature);
@@ -783,8 +1079,8 @@ export class ApiModelGenerator {
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -830,6 +1126,8 @@ export class ApiModelGenerator {
const containerKey: string = ApiProperty.getContainerKey(name, isStatic);
let apiProperty: ApiProperty | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiProperty;
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | DocgenTypedefJson | undefined;
const jsDoc = parent?.props?.find((prop) => prop.name === name);
if (apiProperty === undefined) {
const declaration: ts.Declaration = astDeclaration.declaration;
@@ -857,7 +1155,19 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0;
@@ -877,8 +1187,8 @@ export class ApiModelGenerator {
excerptTokens,
propertyTypeTokenRange,
initializerTokenRange,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
parentApiItem.addMember(apiProperty);
@@ -895,6 +1205,8 @@ export class ApiModelGenerator {
let apiPropertySignature: ApiPropertySignature | undefined = parentApiItem.tryGetMemberByKey(
containerKey,
) as ApiPropertySignature;
const parent = context.parentDocgenJson as DocgenInterfaceJson | DocgenPropertyJson | DocgenTypedefJson | undefined;
const jsDoc = parent?.props?.find((prop) => prop.name === name);
if (apiPropertySignature === undefined) {
const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature;
@@ -906,7 +1218,15 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''
}${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${
'deprecated' in jsDoc && jsDoc.deprecated ? ` * @deprecated ${jsDoc.deprecated}\n` : ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isReadonly: boolean = this._isReadonly(astDeclaration);
@@ -920,8 +1240,8 @@ export class ApiModelGenerator {
excerptTokens,
propertyTypeTokenRange,
isReadonly,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -938,6 +1258,11 @@ export class ApiModelGenerator {
const containerKey: string = ApiTypeAlias.getContainerKey(name);
let apiTypeAlias: ApiTypeAlias | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiTypeAlias;
const parent = context.parentDocgenJson as DocgenJson | undefined;
const jsDoc =
parent?.typedefs.find((type) => type.name === name) ??
parent?.functions.find((func) => func.name === name) ??
parent?.interfaces.find((clas) => clas.name === name);
if (apiTypeAlias === undefined) {
const typeAliasDeclaration: ts.TypeAliasDeclaration = astDeclaration.declaration as ts.TypeAliasDeclaration;
@@ -954,7 +1279,19 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment;
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description) ?? ''}\n${
'params' in jsDoc
? jsDoc.params.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('')
: ''
}${
'returns' in jsDoc
? jsDoc.returns.map((ret) => ` * @returns ${Array.isArray(ret) ? '' : fixLinkTags(ret.description)}\n`)
: ''
} */`,
).docComment
: apiItemMetadata.tsdocComment;
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
const sourceLocation: ISourceLocation = this._getSourceLocation(typeAliasDeclaration);
@@ -966,8 +1303,8 @@ export class ApiModelGenerator {
excerptTokens,
typeTokenRange,
isExported,
fileUrlPath: sourceLocation.sourceFilePath,
fileLine: sourceLocation.sourceFileLine,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
@@ -1021,6 +1358,94 @@ export class ApiModelGenerator {
}
}
// events aren't part of typescript, we only get them from docgen JSON here
private _processApiEvent(context: IProcessAstEntityContext): void {
const { name, parentApiItem } = context;
const containerKey: string = ApiProperty.getContainerKey(name, false);
let apiEvent: ApiEvent | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiEvent;
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const jsDoc = parent?.events?.find((prop) => prop.name === name);
if (apiEvent === undefined && jsDoc) {
const excerptTokens: IExcerptToken[] = [
{
kind: ExcerptTokenKind.Content,
text: `on('${name}', (${
jsDoc.params?.length ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.nullable ? '?' : ''}: ` : ') => {})'
}`,
},
];
const parameters: IApiParameterOptions[] = [];
for (let index = 0; index < (jsDoc.params?.length ?? 0) - 1; index++) {
const parameter = jsDoc.params![index]!;
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
startIndex: excerptTokens.length,
endIndex: excerptTokens.length + newTokens.length,
},
isOptional: Boolean(parameter.optional),
isRest: parameter.name.startsWith('...'),
});
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `, ${jsDoc.params![index + 1]?.name}${jsDoc.params![index + 1]!.optional ? '?' : ''}: `,
});
}
if (jsDoc.params?.length) {
const parameter = jsDoc.params![jsDoc.params.length - 1]!;
const newTokens = this._mapVarType(parameter.type);
parameters.push({
parameterName: parameter.name,
parameterTypeTokenRange: {
startIndex: excerptTokens.length,
endIndex: excerptTokens.length + newTokens.length,
},
isOptional: Boolean(parameter.optional),
isRest: parameter.name.startsWith('...'),
});
excerptTokens.push(...newTokens);
excerptTokens.push({
kind: ExcerptTokenKind.Content,
text: `) => {})`,
});
}
const docComment: tsdoc.DocComment | undefined = new TSDocParser().parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ?? ''
}${'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''}${
'deprecated' in jsDoc && jsDoc.deprecated
? ` * @deprecated ${
typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated
}\n`
: ''
} */`,
).docComment;
const releaseTag: ReleaseTag = ReleaseTag.Public;
apiEvent = new ApiEvent({
name,
docComment,
releaseTag,
excerptTokens,
overloadIndex: 0,
parameters,
fileUrlPath: filePathFromJson(jsDoc.meta),
fileLine: jsDoc.meta.line,
fileColumn: 0,
});
parentApiItem.addMember(apiEvent);
} else {
// If the event was already declared before (via a merged interface declaration),
// we assume its signature is identical, because the language requires that.
}
}
/**
* @param astDeclaration - The declaration
* @param nodesToCapture - A list of child nodes whose token ranges we want to capture
@@ -1131,4 +1556,43 @@ export class ApiModelGenerator {
);
return sourceLocation;
}
private _mapVarType(typey: DocgenVarTypeJson): IExcerptToken[] {
const mapper = Array.isArray(typey) ? typey : typey.types ?? [];
const lookup: { [K in ts.SyntaxKind]?: string } = {
[ts.SyntaxKind.ClassDeclaration]: 'class',
[ts.SyntaxKind.InterfaceDeclaration]: 'interface',
[ts.SyntaxKind.TypeAliasDeclaration]: 'type',
};
return mapper.flatMap((typ) =>
typ.reduce<IExcerptToken[]>(
(arr, [type, symbol]) => [
...arr,
{
kind: ExcerptTokenKind.Reference,
text: type ?? 'unknown',
canonicalReference: DeclarationReference.package(this._apiModel.packages[0]!.name)
.addNavigationStep(Navigation.Members as any, DeclarationReference.parseComponent(type ?? 'unknown'))
.withMeaning(
lookup[
(
(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
)?.astSymbol
)?.astDeclarations[0]?.declaration.kind ?? ts.SyntaxKind.ClassDeclaration
] ?? ('class' as any),
)
.toString(),
},
{ kind: ExcerptTokenKind.Content, text: symbol ?? '' },
],
[],
),
);
}
}

View File

@@ -1,7 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { ExcerptTokenKind, type IExcerptToken, type IExcerptTokenRange } from '@discordjs/api-extractor-model';
import {
ExcerptTokenKind,
type IExcerptToken,
type IExcerptTokenRange,
type IExcerptTokenRangeWithTypeParameters,
} from '@discordjs/api-extractor-model';
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference';
import * as ts from 'typescript';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
@@ -126,6 +131,10 @@ export class ExcerptBuilder {
return { startIndex: 0, endIndex: 0 };
}
public static createEmptyTokenRangeWithTypeParameters(): IExcerptTokenRangeWithTypeParameters {
return { startIndex: 0, endIndex: 0, typeParameters: [] };
}
private static _buildSpan(excerptTokens: IExcerptToken[], span: Span, state: IBuildSpanState): boolean {
if (span.kind === ts.SyntaxKind.JSDocComment) {
// Discard any comments

View File

@@ -3,7 +3,7 @@
/**
* API Extractor helps with validation, documentation, and reviewing of the exported API for a TypeScript library.
* The `@microsoft/api-extractor` package provides the command-line tool. It also exposes a developer API that you
* The `@discordjs/api-extractor` package provides the command-line tool. It also exposes a developer API that you
* can use to invoke API Extractor programmatically.
*
* @packageDocumentation