feat(website): show descriptions for @typeParam blocks (#8523)

This commit is contained in:
Suneet Tipirneni
2022-08-19 04:55:43 -04:00
committed by GitHub
parent 673262d38c
commit e475b63f25
18 changed files with 126 additions and 62 deletions

View File

@@ -22,6 +22,8 @@ export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalAction
/** /**
* Represents an action row component * Represents an action row component
*
* @typeParam T - The types of components this action row holds
*/ */
export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBuilder< export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBuilder<
APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent> APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>
@@ -56,6 +58,9 @@ export class ActionRowBuilder<T extends AnyComponentBuilder> extends ComponentBu
return this; return this;
} }
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> { public toJSON(): APIActionRowComponent<ReturnType<T['toJSON']>> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return { return {

View File

@@ -10,6 +10,8 @@ export type AnyAPIActionRowComponent = APIActionRowComponentTypes | APIActionRow
/** /**
* Represents a discord component * Represents a discord component
*
* @typeParam DataType - The type of internal API data that is stored within the component
*/ */
export abstract class ComponentBuilder< export abstract class ComponentBuilder<
DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>, DataType extends Partial<APIBaseComponent<ComponentType>> = APIBaseComponent<ComponentType>,
@@ -20,6 +22,9 @@ export abstract class ComponentBuilder<
*/ */
public readonly data: Partial<DataType>; public readonly data: Partial<DataType>;
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public abstract toJSON(): AnyAPIActionRowComponent; public abstract toJSON(): AnyAPIActionRowComponent;
public constructor(data: Partial<DataType>) { public constructor(data: Partial<DataType>) {

View File

@@ -85,6 +85,9 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
return this; return this;
} }
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public toJSON(): APIButtonComponent { public toJSON(): APIButtonComponent {
validateRequiredButtonParameters( validateRequiredButtonParameters(
this.data.style, this.data.style,

View File

@@ -116,6 +116,9 @@ export class SelectMenuBuilder extends ComponentBuilder<APISelectMenuComponent>
return this; return this;
} }
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public toJSON(): APISelectMenuComponent { public toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.data.custom_id); validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions

View File

@@ -1,4 +1,5 @@
import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10';
import type { JSONEncodable } from '../../util/jsonEncodable';
import { import {
defaultValidator, defaultValidator,
@@ -10,7 +11,7 @@ import {
/** /**
* Represents a option within a select menu component * Represents a option within a select menu component
*/ */
export class SelectMenuOptionBuilder { export class SelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
public constructor(public data: Partial<APISelectMenuOption> = {}) {} public constructor(public data: Partial<APISelectMenuOption> = {}) {}
/** /**
@@ -63,6 +64,9 @@ export class SelectMenuOptionBuilder {
return this; return this;
} }
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public toJSON(): APISelectMenuOption { public toJSON(): APISelectMenuOption {
validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value); validateRequiredSelectMenuOptionParameters(this.data.label, this.data.value);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions

View File

@@ -10,11 +10,15 @@ import {
labelValidator, labelValidator,
textInputStyleValidator, textInputStyleValidator,
} from './Assertions'; } from './Assertions';
import type { Equatable } from '../../util/equatable';
import { isJSONEncodable, type JSONEncodable } from '../../util/jsonEncodable'; import { isJSONEncodable, type JSONEncodable } from '../../util/jsonEncodable';
import { customIdValidator } from '../Assertions'; import { customIdValidator } from '../Assertions';
import { ComponentBuilder } from '../Component'; import { ComponentBuilder } from '../Component';
export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> { export class TextInputBuilder
extends ComponentBuilder<APITextInputComponent>
implements Equatable<JSONEncodable<APITextInputComponent> | APITextInputComponent>
{
public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) {
super({ type: ComponentType.TextInput, ...data }); super({ type: ComponentType.TextInput, ...data });
} }
@@ -99,6 +103,9 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
return this; return this;
} }
/**
* {@inheritDoc JSONEncodable.toJSON}
*/
public toJSON(): APITextInputComponent { public toJSON(): APITextInputComponent {
validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label); validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -107,6 +114,9 @@ export class TextInputBuilder extends ComponentBuilder<APITextInputComponent> {
} as APITextInputComponent; } as APITextInputComponent;
} }
/**
* {@inheritDoc Equatable.equals}
*/
public equals(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): boolean { public equals(other: JSONEncodable<APITextInputComponent> | APITextInputComponent): boolean {
if (isJSONEncodable(other)) { if (isJSONEncodable(other)) {
return isEqual(other.toJSON(), this.data); return isEqual(other.toJSON(), this.data);

View File

@@ -49,7 +49,7 @@ export class SlashCommandBuilder {
* Whether the command is enabled by default when the app is added to a guild * Whether the command is enabled by default when the app is added to a guild
* *
* @deprecated This property is deprecated and will be removed in the future. * @deprecated This property is deprecated and will be removed in the future.
* You should use `setDefaultMemberPermissions` or `setDMPermission` instead. * You should use {@link (SlashCommandBuilder:class).setDefaultMemberPermissions} or {@link (SlashCommandBuilder:class).setDMPermission} instead.
*/ */
public readonly default_permission: boolean | undefined = undefined; public readonly default_permission: boolean | undefined = undefined;
@@ -89,7 +89,7 @@ export class SlashCommandBuilder {
* @param value - Whether or not to enable this command by default * @param value - Whether or not to enable this command by default
* *
* @see https://discord.com/developers/docs/interactions/application-commands#permissions * @see https://discord.com/developers/docs/interactions/application-commands#permissions
* @deprecated Use `setDefaultMemberPermissions` or `setDMPermission` instead. * @deprecated Use {@link (SlashCommandBuilder:class).setDefaultMemberPermissions} or {@link (SlashCommandBuilder:class).setDMPermission} instead.
*/ */
public setDefaultPermission(value: boolean) { public setDefaultPermission(value: boolean) {
// Assert the value matches the conditions // Assert the value matches the conditions

View File

@@ -1,3 +1,9 @@
/**
* Represents a structure that can be checked against another
* given structure for equality
*
* @typeParam T - The type of object to compare the current object to
*/
export interface Equatable<T> { export interface Equatable<T> {
/** /**
* Whether or not this is equal to another structure * Whether or not this is equal to another structure

View File

@@ -1,3 +1,8 @@
/**
* Represents an object capable of representing itself as a JSON object
*
* @typeParam T - The JSON type corresponding to {@link JSONEncodable.toJSON} outputs.
*/
export interface JSONEncodable<T> { export interface JSONEncodable<T> {
/** /**
* Transforms this object to its JSON format * Transforms this object to its JSON format

View File

@@ -28,6 +28,9 @@ export interface Collection<K, V> extends Map<K, V> {
/** /**
* A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has * A Map with additional utility methods. This is used throughout discord.js rather than Arrays for anything that has
* an ID, for significantly improved performance and ease-of-use. * an ID, for significantly improved performance and ease-of-use.
*
* @typeParam K - The key type this collection holds
* @typeParam V - The value type this collection holds
*/ */
export class Collection<K, V> extends Map<K, V> { export class Collection<K, V> extends Map<K, V> {
/** /**

View File

@@ -1,6 +1,32 @@
import type { ApiItem, ApiModel, ApiTypeParameterListMixin } from '@microsoft/api-extractor-model'; import type { ApiItem, ApiModel, ApiTypeParameterListMixin, TypeParameter } from '@microsoft/api-extractor-model';
import type { DocItemConstructor } from './DocItem'; import type { DocItemConstructor } from './DocItem';
import { generateTypeParamData, TypeParameterData } from '~/util/parse.server'; import { block, DocBlockJSON } from './comment/CommentBlock';
import { genToken, TokenDocumentation } from '~/util/parse.server';
export interface TypeParameterData {
name: string;
constraintTokens: TokenDocumentation[];
defaultTokens: TokenDocumentation[];
optional: boolean;
commentBlock: DocBlockJSON | null;
}
export function generateTypeParamData(
model: ApiModel,
typeParam: TypeParameter,
parentItem?: ApiItem,
): TypeParameterData {
const constraintTokens = typeParam.constraintExcerpt.spannedTokens.map((token) => genToken(model, token));
const defaultTokens = typeParam.defaultTypeExcerpt.spannedTokens.map((token) => genToken(model, token));
return {
name: typeParam.name,
constraintTokens,
defaultTokens,
optional: typeParam.isOptional,
commentBlock: typeParam.tsdocTypeParamBlock ? block(typeParam.tsdocTypeParamBlock, model, parentItem) : null,
};
}
export function TypeParameterMixin<TBase extends DocItemConstructor>(Base: TBase) { export function TypeParameterMixin<TBase extends DocItemConstructor>(Base: TBase) {
return class Mixed extends Base { return class Mixed extends Base {
@@ -10,7 +36,7 @@ export function TypeParameterMixin<TBase extends DocItemConstructor>(Base: TBase
public constructor(model: ApiModel, item: ApiItem) { public constructor(model: ApiModel, item: ApiItem) {
super(model, item); super(model, item);
this.typeParameters = (item as ApiTypeParameterListMixin).typeParameters.map((typeParam) => this.typeParameters = (item as ApiTypeParameterListMixin).typeParameters.map((typeParam) =>
generateTypeParamData(this.model, typeParam), generateTypeParamData(this.model, typeParam, item.parent),
); );
} }

View File

@@ -14,11 +14,7 @@ export function genToken(
ref: DocDeclarationReference, ref: DocDeclarationReference,
context: ApiItem | null, context: ApiItem | null,
): LinkTagCodeLink | null { ): LinkTagCodeLink | null {
if (!context) { const item = model.resolveDeclarationReference(ref, context ?? undefined).resolvedApiItem ?? null;
return null;
}
const item = model.resolveDeclarationReference(ref, context).resolvedApiItem ?? null;
if (!item) { if (!item) {
return null; return null;
@@ -38,8 +34,14 @@ export interface LinkTagCodeLink {
} }
export function linkTagNode(linkNode: DocLinkTag, model: ApiModel, parentItem?: ApiItem): DocLinkTagJSON { export function linkTagNode(linkNode: DocLinkTag, model: ApiModel, parentItem?: ApiItem): DocLinkTagJSON {
const codeDestination = // If we weren't provided a parent object, fallback to the package entrypoint.
linkNode.codeDestination && parentItem ? genToken(model, linkNode.codeDestination, parentItem) : null; const packageEntryPoint = linkNode.codeDestination?.importPath
? model.getAssociatedPackage()?.findEntryPointsByPath(linkNode.codeDestination.importPath)[0]
: null;
const codeDestination = linkNode.codeDestination
? genToken(model, linkNode.codeDestination, parentItem ?? packageEntryPoint ?? null)
: null;
const text = linkNode.linkText ?? null; const text = linkNode.linkText ?? null;
const urlDestination = linkNode.urlDestination ?? null; const urlDestination = linkNode.urlDestination ?? null;

View File

@@ -1,6 +1,6 @@
import { Group, Stack, Title, Text, Box, MediaQuery, Aside, ScrollArea } from '@mantine/core'; import { Group, Stack, Title, Text, Box, MediaQuery, Aside, ScrollArea } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import type { ReactNode } from 'react'; import { Fragment, ReactNode } from 'react';
import { VscListSelection, VscSymbolParameter } from 'react-icons/vsc'; import { VscListSelection, VscSymbolParameter } from 'react-icons/vsc';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
@@ -11,9 +11,10 @@ import { TypeParamTable } from './TypeParamTable';
import { TSDoc } from './tsdoc/TSDoc'; import { TSDoc } from './tsdoc/TSDoc';
import type { DocClass } from '~/DocModel/DocClass'; import type { DocClass } from '~/DocModel/DocClass';
import type { DocItem } from '~/DocModel/DocItem'; import type { DocItem } from '~/DocModel/DocItem';
import type { TypeParameterData } from '~/DocModel/TypeParameterMixin';
import type { AnyDocNodeJSON } from '~/DocModel/comment/CommentNode'; import type { AnyDocNodeJSON } from '~/DocModel/comment/CommentNode';
import { generateIcon } from '~/util/icon'; import { generateIcon } from '~/util/icon';
import type { TokenDocumentation, TypeParameterData } from '~/util/parse.server'; import type { TokenDocumentation } from '~/util/parse.server';
export interface DocContainerProps { export interface DocContainerProps {
name: string; name: string;
@@ -84,10 +85,10 @@ export function DocContainer({
</Title> </Title>
<Text sx={{ wordBreak: 'break-all' }} className="font-mono"> <Text sx={{ wordBreak: 'break-all' }} className="font-mono">
{implementsTokens.map((token, idx) => ( {implementsTokens.map((token, idx) => (
<> <Fragment key={idx}>
<HyperlinkedText tokens={token} /> <HyperlinkedText tokens={token} />
{idx < implementsTokens.length - 1 ? ', ' : ''} {idx < implementsTokens.length - 1 ? ', ' : ''}
</> </Fragment>
))} ))}
</Text> </Text>
</Group> </Group>

View File

@@ -1,4 +1,5 @@
import { Divider, Stack } from '@mantine/core'; import { Divider, Stack } from '@mantine/core';
import { Fragment } from 'react';
import { MethodItem } from './MethodItem'; import { MethodItem } from './MethodItem';
import type { DocMethod } from '~/DocModel/DocMethod'; import type { DocMethod } from '~/DocModel/DocMethod';
import type { DocMethodSignature } from '~/DocModel/DocMethodSignature'; import type { DocMethodSignature } from '~/DocModel/DocMethodSignature';
@@ -11,13 +12,12 @@ export function MethodList({
return ( return (
<Stack> <Stack>
{data.map((method) => ( {data.map((method) => (
<> <Fragment
<MethodItem key={`${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`}
key={`${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`} >
data={method} <MethodItem data={method} />
/>
<Divider className="bg-gray-100" size="md" /> <Divider className="bg-gray-100" size="md" />
</> </Fragment>
))} ))}
</Stack> </Stack>
); );

View File

@@ -1,7 +1,8 @@
import { Box } from '@mantine/core'; import { Box, ScrollArea } from '@mantine/core';
import { HyperlinkedText } from './HyperlinkedText'; import { HyperlinkedText } from './HyperlinkedText';
import { Table } from './Table'; import { Table } from './Table';
import type { TypeParameterData } from '~/util/parse.server'; import { TSDoc } from './tsdoc/TSDoc';
import type { TypeParameterData } from '~/DocModel/TypeParameterMixin';
export function TypeParamTable({ data }: { data: TypeParameterData[] }) { export function TypeParamTable({ data }: { data: TypeParameterData[] }) {
const rows = data.map((typeParam) => ({ const rows = data.map((typeParam) => ({
@@ -9,22 +10,24 @@ export function TypeParamTable({ data }: { data: TypeParameterData[] }) {
Constraints: <HyperlinkedText tokens={typeParam.constraintTokens} />, Constraints: <HyperlinkedText tokens={typeParam.constraintTokens} />,
Optional: typeParam.optional ? 'Yes' : 'No', Optional: typeParam.optional ? 'Yes' : 'No',
Default: <HyperlinkedText tokens={typeParam.defaultTokens} />, Default: <HyperlinkedText tokens={typeParam.defaultTokens} />,
Description: 'None', Description: typeParam.commentBlock ? <TSDoc node={typeParam.commentBlock} /> : 'None',
})); }));
const rowElements = { const rowElements = {
Name: 'font-mono', Name: 'font-mono whitespace-nowrap',
Constraints: 'font-mono', Constraints: 'font-mono whitespace-pre break-normal',
Default: 'font-mono', Default: 'font-mono whitespace-pre break-normal',
}; };
return ( return (
<Box sx={{ overflowX: 'auto' }}> <Box sx={{ overflowX: 'auto' }}>
<Table <ScrollArea type="auto">
columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']} <Table
rows={rows} columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']}
columnStyles={rowElements} rows={rows}
/> columnStyles={rowElements}
/>
</ScrollArea>
</Box> </Box>
); );
} }

View File

@@ -52,6 +52,7 @@ export function BlockComment({ children, tagName, index }: BlockCommentProps): J
return <DeprecatedBlock>{children}</DeprecatedBlock>; return <DeprecatedBlock>{children}</DeprecatedBlock>;
case StandardTags.remarks.tagNameWithUpperCase: case StandardTags.remarks.tagNameWithUpperCase:
return <RemarksBlock>{children}</RemarksBlock>; return <RemarksBlock>{children}</RemarksBlock>;
case StandardTags.typeParam.tagNameWithUpperCase:
case StandardTags.param.tagNameWithUpperCase: case StandardTags.param.tagNameWithUpperCase:
return <Text>{children}</Text>; return <Text>{children}</Text>;
default: // TODO: Support more blocks in the future. default: // TODO: Support more blocks in the future.

View File

@@ -1,7 +1,7 @@
import { Anchor, Box, Code, Text } from '@mantine/core'; import { Anchor, Box, Code, Text } from '@mantine/core';
import { DocNodeKind, StandardTags } from '@microsoft/tsdoc'; import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import Link from 'next/link'; import Link from 'next/link';
import type { ReactNode } from 'react'; import { Fragment, ReactNode } from 'react';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { BlockComment } from './BlockComment'; import { BlockComment } from './BlockComment';
@@ -32,7 +32,7 @@ export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element {
</Text> </Text>
); );
case DocNodeKind.SoftBreak: case DocNodeKind.SoftBreak:
return <></>; return <Fragment key={idx} />;
case DocNodeKind.LinkTag: { case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, text } = node as DocLinkTagJSON; const { codeDestination, urlDestination, text } = node as DocLinkTagJSON;

View File

@@ -10,7 +10,6 @@ import {
type ApiPropertyItem, type ApiPropertyItem,
type ExcerptToken, type ExcerptToken,
type Parameter, type Parameter,
type TypeParameter,
ApiFunction, ApiFunction,
} from '@microsoft/api-extractor-model'; } from '@microsoft/api-extractor-model';
import type { DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc'; import type { DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc';
@@ -25,7 +24,7 @@ export function findPackage(model: ApiModel, name: string): ApiPackage | undefin
} }
export function generatePath(items: readonly ApiItem[]) { export function generatePath(items: readonly ApiItem[]) {
let path = '/docs/main/packages/'; let path = '/docs/main/packages';
for (const item of items) { for (const item of items) {
switch (item.kind) { switch (item.kind) {
case ApiItemKind.Model: case ApiItemKind.Model:
@@ -33,17 +32,24 @@ export function generatePath(items: readonly ApiItem[]) {
case ApiItemKind.EnumMember: case ApiItemKind.EnumMember:
break; break;
case ApiItemKind.Package: case ApiItemKind.Package:
path += `${item.displayName}/`; path += `/${item.displayName}`;
break; break;
case ApiItemKind.Function: case ApiItemKind.Function:
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const functionItem = item as ApiFunction; const functionItem = item as ApiFunction;
path += `${functionItem.displayName}${ path += `/${functionItem.displayName}${
functionItem.overloadIndex && functionItem.overloadIndex > 1 ? `:${functionItem.overloadIndex}` : '' functionItem.overloadIndex && functionItem.overloadIndex > 1 ? `:${functionItem.overloadIndex}` : ''
}/`; }`;
break;
case ApiItemKind.Property:
case ApiItemKind.Method:
case ApiItemKind.MethodSignature:
case ApiItemKind.PropertySignature:
// TODO: Take overloads into account
path += `#${item.displayName}`;
break; break;
default: default:
path += `${item.displayName}/`; path += `/${item.displayName}`;
} }
} }
@@ -213,22 +219,3 @@ export function getMembers(pkg: ApiPackage) {
overloadIndex: member.kind === 'Function' ? (member as ApiFunction).overloadIndex : null, overloadIndex: member.kind === 'Function' ? (member as ApiFunction).overloadIndex : null,
})); }));
} }
export interface TypeParameterData {
name: string;
constraintTokens: TokenDocumentation[];
defaultTokens: TokenDocumentation[];
optional: boolean;
}
export function generateTypeParamData(model: ApiModel, typeParam: TypeParameter): TypeParameterData {
const constraintTokens = typeParam.constraintExcerpt.spannedTokens.map((token) => genToken(model, token));
const defaultTokens = typeParam.defaultTypeExcerpt.spannedTokens.map((token) => genToken(model, token));
return {
name: typeParam.name,
constraintTokens,
defaultTokens,
optional: typeParam.isOptional,
};
}