refactor(website): extract layouts and use more server components (#9027)

Closes https://github.com/discordjs/discord.js/issues/8920
Closes https://github.com/discordjs/discord.js/issues/8997
This commit is contained in:
Suneet Tipirneni
2023-01-10 12:25:14 -05:00
committed by GitHub
parent 158db474b7
commit 39c4de2dbc
73 changed files with 1831 additions and 1476 deletions

View File

@@ -0,0 +1,8 @@
import type { PropsWithChildren } from 'react';
/**
* Layout parent of documentation pages.
*/
export function Documentation({ children }: PropsWithChildren) {
return <div className="w-full flex-col space-y-4">{children}</div>;
}

View File

@@ -0,0 +1,36 @@
import { ApiItemKind } from '@microsoft/api-extractor-model';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import type { PropsWithChildren } from 'react';
function generateIcon(kind: ApiItemKind) {
switch (kind) {
case ApiItemKind.Class:
return <VscSymbolClass />;
case ApiItemKind.Function:
case ApiItemKind.Method:
return <VscSymbolMethod />;
case ApiItemKind.Enum:
return <VscSymbolEnum />;
case ApiItemKind.Interface:
return <VscSymbolInterface />;
case ApiItemKind.TypeAlias:
return <VscSymbolVariable />;
default:
return <VscSymbolMethod />;
}
}
export function Header({ kind, name }: PropsWithChildren<{ kind: ApiItemKind; name: string }>) {
return (
<div className="flex flex-col">
<h2 className="flex flex-row place-items-center gap-2 break-all text-2xl font-bold">
<span>{generateIcon(kind)}</span>
{name}
</h2>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import type { ApiClass, ApiInterface, Excerpt } from '@microsoft/api-extractor-model';
import { ApiItemKind } from '@microsoft/api-extractor-model';
import { ExcerptText } from '../ExcerptText';
export function HierarchyText({ item, type }: { item: ApiClass | ApiInterface; type: 'Extends' | 'Implements' }) {
const model = item.getAssociatedModel()!;
if (
(item.kind === ApiItemKind.Class &&
(item as ApiClass).extendsType === undefined &&
(item as ApiClass).implementsTypes.length === 0) ||
(item.kind === ApiItemKind.Interface && !(item as ApiInterface).extendsTypes)
) {
return null;
}
let excerpts: Excerpt[];
if (item.kind === ApiItemKind.Class) {
if (type === 'Implements') {
if ((item as ApiClass).implementsTypes.length === 0) {
return null;
}
excerpts = (item as ApiClass).implementsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
} else {
if (!(item as ApiClass).extendsType) {
return null;
}
excerpts = [(item as ApiClass).extendsType!.excerpt];
}
} else {
if ((item as ApiInterface).extendsTypes.length === 0) {
return null;
}
excerpts = (item as ApiInterface).extendsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
}
return (
<div className="flex flex-row place-items-center gap-4">
<h3 className="text-xl font-bold">{type}</h3>
<span className="space-y-2 break-all font-mono">
{excerpts.map((excerpt, index) => (
<ExcerptText excerpt={excerpt} key={index} model={model} />
))}
</span>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import type { ApiDeclaredItem, ApiItemContainerMixin, ApiTypeParameterListMixin } from '@microsoft/api-extractor-model';
import type { ReactNode } from 'react';
import { Outline } from '../Outline';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { Documentation } from './Documentation';
import { MethodsSection } from './section/MethodsSection';
import { PropertiesSection } from './section/PropertiesSection';
import { SummarySection } from './section/SummarySection';
import { TypeParameterSection } from './section/TypeParametersSection';
import { hasProperties, hasMethods, serializeMembers } from './util';
export function MemberContainerDocumentation({
item,
version,
subheading,
}: {
item: ApiDeclaredItem & ApiItemContainerMixin & ApiTypeParameterListMixin;
subheading?: ReactNode;
version: string;
}) {
return (
<Documentation item={item}>
{subheading}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}
{hasProperties(item) ? <PropertiesSection item={item} /> : null}
{hasMethods(item) ? <MethodsSection item={item} /> : null}
<Outline members={serializeMembers(item)} />
</Documentation>
);
}

View File

@@ -0,0 +1,13 @@
import type { ApiDeclaredItem, ApiItemContainerMixin } from '@microsoft/api-extractor-model';
import { MethodsSection } from './section/MethodsSection';
import { PropertiesSection } from './section/PropertiesSection';
import { hasProperties, hasMethods } from './util';
export function Members({ item }: { item: ApiDeclaredItem & ApiItemContainerMixin }) {
return (
<>
{hasProperties(item) ? <PropertiesSection item={item} /> : null}
{hasMethods(item) ? <MethodsSection item={item} /> : null}
</>
);
}

View File

@@ -0,0 +1,18 @@
import type { ApiDeclaredItem, ApiItemContainerMixin } from '@microsoft/api-extractor-model';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { Header } from './Header';
import { SummarySection } from './section/SummarySection';
export interface ObjectHeaderProps {
item: ApiDeclaredItem & ApiItemContainerMixin;
}
export function ObjectHeader({ item }: ObjectHeaderProps) {
return (
<>
<Header kind={item.kind} name={item.displayName} />
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
</>
);
}

View File

@@ -0,0 +1,30 @@
import type { ApiConstructor } from '@microsoft/api-extractor-model';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { useCallback } from 'react';
import { TSDoc } from '../tsdoc/TSDoc';
import { ResponsiveSection } from './ResponsiveSection';
import { ParameterTable } from '~/components/ParameterTable';
export function ConstructorSection({ item }: { item: ApiConstructor }) {
const getShorthandName = useCallback(
(ctor: ApiConstructor) =>
`constructor(${ctor.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`,
[],
);
return (
<ResponsiveSection icon={<VscSymbolMethod size={20} />} padded title="Constructor">
<div className="flex flex-col gap-2">
<h4 className="break-all font-mono text-lg font-bold">{getShorthandName(item)}</h4>
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
<ParameterTable item={item} />
</div>
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,45 @@
import type {
ApiDeclaredItem,
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
} from '@microsoft/api-extractor-model';
import { ApiItemKind } from '@microsoft/api-extractor-model';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { useMemo, Fragment } from 'react';
import { ResponsiveSection } from './ResponsiveSection';
import { Method } from '~/components/model/method/Method';
import { resolveMembers } from '~/util/members';
function isMethodLike(item: ApiItem): item is ApiMethod | ApiMethodSignature {
return (
item.kind === ApiItemKind.Method ||
(item.kind === ApiItemKind.MethodSignature && (item as ApiMethod).overloadIndex <= 1)
);
}
export function MethodsSection({ item }: { item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isMethodLike);
const methodItems = useMemo(
() =>
members.map(({ item: method, inherited }) => (
<Fragment
key={`${method.displayName}${
method.overloadIndex && method.overloadIndex > 1 ? `:${(method as ApiMethod).overloadIndex}` : ''
}`}
>
<Method inheritedFrom={inherited as ApiDeclaredItem & ApiItemContainerMixin} method={method} />
<div className="border-light-900 dark:border-dark-100 -mx-8 border-t-2" />
</Fragment>
)),
[members],
);
return (
<ResponsiveSection icon={<VscSymbolMethod size={20} />} padded title="Methods">
<div className="flex flex-col gap-4">{methodItems}</div>
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,12 @@
import type { ApiParameterListMixin } from '@microsoft/api-extractor-model';
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ResponsiveSection } from './ResponsiveSection';
import { ParameterTable } from '~/components/ParameterTable';
export function ParameterSection({ item }: { item: ApiParameterListMixin }) {
return (
<ResponsiveSection icon={<VscSymbolParameter size={20} />} padded title="Parameters">
<ParameterTable item={item} />
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,12 @@
import type { ApiItemContainerMixin } from '@microsoft/api-extractor-model';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { ResponsiveSection } from './ResponsiveSection';
import { PropertyList } from '~/components/PropertyList';
export function PropertiesSection({ item }: { item: ApiItemContainerMixin }) {
return (
<ResponsiveSection icon={<VscSymbolProperty size={20} />} padded title="Properties">
<PropertyList item={item} />
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
import type { SectionOptions } from '@discordjs/ui';
import { Section } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
import { useMedia } from 'react-use';
export function ResponsiveSection(opts: PropsWithChildren<SectionOptions & { separator?: boolean }>) {
const matches = useMedia('(max-width: 768px)', true);
const { children, separator, ...rest } = opts;
const props = {
...rest,
dense: matches,
};
return (
<Section {...props}>
{children}
{separator ? <div className="border-light-900 dark:border-dark-100 -mx-8 mt-6 border-t-2" /> : null}
</Section>
);
}

View File

@@ -0,0 +1,16 @@
import type { ApiDeclaredItem } from '@microsoft/api-extractor-model';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { TSDoc } from '../tsdoc/TSDoc';
import { ResponsiveSection } from './ResponsiveSection';
export function SummarySection({ item }: { item: ApiDeclaredItem }) {
return (
<ResponsiveSection icon={<VscListSelection size={20} />} padded separator title="Summary">
{item.tsdocComment?.summarySection ? (
<TSDoc item={item} tsdoc={item.tsdocComment} />
) : (
<p>No summary provided.</p>
)}
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,12 @@
import type { ApiTypeParameterListMixin } from '@microsoft/api-extractor-model';
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ResponsiveSection } from './ResponsiveSection';
import { TypeParamTable } from '~/components/TypeParamTable';
export function TypeParameterSection({ item }: { item: ApiTypeParameterListMixin }) {
return (
<ResponsiveSection icon={<VscSymbolParameter size={20} />} padded title="Type Parameters">
<TypeParamTable item={item} />
</ResponsiveSection>
);
}

View File

@@ -0,0 +1,38 @@
import { Alert } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
export function Block({ children, title }: PropsWithChildren<{ title: string }>) {
return (
<div className="flex flex-col gap-2">
<h5 className="font-bold">{title}</h5>
{children}
</div>
);
}
export function ExampleBlock({
children,
exampleIndex,
}: PropsWithChildren<{ exampleIndex?: number | undefined }>): JSX.Element {
return <Block title={`Example ${exampleIndex ? exampleIndex : ''}`}>{children}</Block>;
}
export function DefaultValueBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Default value">{children}</Block>;
}
export function RemarksBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Remarks">{children}</Block>;
}
export function DeprecatedBlock({ children }: PropsWithChildren): JSX.Element {
return (
<Alert title="Deprecated" type="danger">
{children}
</Alert>
);
}
export function SeeBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="See Also">{children}</Block>;
}

View File

@@ -0,0 +1,121 @@
import type { ApiItem } from '@microsoft/api-extractor-model';
import type { DocComment, DocFencedCode, DocLinkTag, DocNode, DocNodeContainer, DocPlainText } from '@microsoft/tsdoc';
import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import Link from 'next/link';
import { Fragment, useCallback, type ReactNode } from 'react';
import { SyntaxHighlighter } from '../../SyntaxHighlighter';
import { resolveItemURI } from '../util';
import { DeprecatedBlock, ExampleBlock, RemarksBlock, SeeBlock } from './BlockComment';
import { ItemLink } from '~/components/ItemLink';
export function TSDoc({ item, tsdoc }: { item: ApiItem; tsdoc: DocNode }): JSX.Element {
const createNode = useCallback(
(tsdoc: DocNode, idx?: number): ReactNode => {
switch (tsdoc.kind) {
case DocNodeKind.PlainText:
return (
<span className="break-words" key={idx}>
{(tsdoc as DocPlainText).text}
</span>
);
case DocNodeKind.Section:
case DocNodeKind.Paragraph:
return (
<span className="break-words leading-relaxed" key={idx}>
{(tsdoc as DocNodeContainer).nodes.map((node, idx) => createNode(node, idx))}
</span>
);
case DocNodeKind.SoftBreak:
return <Fragment key={idx} />;
case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, linkText } = tsdoc as DocLinkTag;
if (codeDestination) {
const foundItem = item
.getAssociatedModel()
?.resolveDeclarationReference(codeDestination, item).resolvedApiItem;
if (!foundItem) return null;
return (
<ItemLink
className="text-blurple focus:ring-width-2 focus:ring-blurple rounded font-mono outline-0 focus:ring"
itemURI={resolveItemURI(foundItem)}
key={idx}
>
{linkText ?? foundItem.displayName}
</ItemLink>
);
}
if (urlDestination) {
return (
<Link
className="text-blurple focus:ring-width-2 focus:ring-blurple rounded font-mono outline-0 focus:ring"
href={urlDestination}
key={idx}
>
{linkText ?? urlDestination}
</Link>
);
}
return null;
}
case DocNodeKind.CodeSpan: {
const { code } = tsdoc as DocFencedCode;
return (
<code className="font-mono text-sm" key={idx}>
{code}
</code>
);
}
case DocNodeKind.FencedCode: {
const { language, code } = tsdoc as DocFencedCode;
return <SyntaxHighlighter code={code} key={idx} language={language} />;
}
case DocNodeKind.Comment: {
const comment = tsdoc as DocComment;
const exampleBlocks = comment.customBlocks.filter(
(block) => block.blockTag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase,
);
return (
<div className="flex flex-col space-y-2">
{comment.deprecatedBlock ? (
<DeprecatedBlock>{createNode(comment.deprecatedBlock.content)}</DeprecatedBlock>
) : null}
{comment.summarySection ? createNode(comment.summarySection) : null}
{comment.remarksBlock ? <RemarksBlock>{createNode(comment.remarksBlock.content)}</RemarksBlock> : null}
{exampleBlocks.length
? exampleBlocks.map((block, idx) => <ExampleBlock key={idx}>{createNode(block.content)}</ExampleBlock>)
: null}
{comment.seeBlocks.length ? (
<SeeBlock>{comment.seeBlocks.map((seeBlock, idx) => createNode(seeBlock.content, idx))}</SeeBlock>
) : null}
</div>
);
}
default:
// console.log(`Captured unknown node kind: ${node.kind}`);
return null;
}
},
[item],
);
return (
<>
{tsdoc.kind === 'Paragraph' || tsdoc.kind === 'Section' ? (
<>{(tsdoc as DocNodeContainer).nodes.map((node, idx) => createNode(node, idx))}</>
) : (
createNode(tsdoc)
)}
</>
);
}

View File

@@ -0,0 +1,53 @@
import { ApiItemKind } from '@microsoft/api-extractor-model';
import type {
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
ApiProperty,
ApiPropertySignature,
} from '@microsoft/api-extractor-model';
import type { TableOfContentsSerialized } from '../TableOfContentItems';
import { resolveMembers } from '~/util/members';
export function hasProperties(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(
({ item: member }) => member.kind === ApiItemKind.Property || member.kind === ApiItemKind.PropertySignature,
);
}
export function hasMethods(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(
({ item: member }) => member.kind === ApiItemKind.Method || member.kind === ApiItemKind.MethodSignature,
);
}
export function resolveItemURI(item: ApiItem): string {
return `/${item.displayName}:${item.kind}`;
}
function memberPredicate(item: ApiItem): item is ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature {
return (
item.kind === ApiItemKind.Property ||
item.kind === ApiItemKind.PropertySignature ||
item.kind === ApiItemKind.Method ||
item.kind === ApiItemKind.MethodSignature
);
}
export function serializeMembers(clazz: ApiItemContainerMixin): TableOfContentsSerialized[] {
return resolveMembers(clazz, memberPredicate).map(({ item: member }) => {
if (member.kind === 'Method' || member.kind === 'MethodSignature') {
return {
kind: member.kind as 'Method' | 'MethodSignature',
name: member.displayName,
};
} else {
return {
kind: member.kind as 'Property' | 'PropertySignature',
name: member.displayName,
overloadIndex: (member as ApiMethod | ApiMethodSignature).overloadIndex,
};
}
});
}