mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +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:
@@ -60,7 +60,7 @@
|
||||
"lucide-react": "^0.503.0",
|
||||
"meilisearch": "^0.49.0",
|
||||
"motion": "^12.9.2",
|
||||
"next": "15.4.0-canary.11",
|
||||
"next": "15.4.0-canary.31",
|
||||
"next-mdx-remote-client": "^2.1.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.4.3",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted'
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Suspense, type PropsWithChildren } from 'react';
|
||||
import { EntryPointSelect } from '@/components/EntrypointSelect';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Navigation } from '@/components/Navigation';
|
||||
import { Scrollbars } from '@/components/OverlayScrollbars';
|
||||
@@ -13,14 +14,21 @@ import { ThemeSwitchNoSRR } from '@/components/ThemeSwitch';
|
||||
import { VersionSelect } from '@/components/VersionSelect';
|
||||
import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarTrigger } from '@/components/ui/Sidebar';
|
||||
import { buttonStyles } from '@/styles/ui/button';
|
||||
import { PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants';
|
||||
import { ENV } from '@/util/env';
|
||||
import { fetchEntryPoints } from '@/util/fetchEntryPoints';
|
||||
import { fetchVersions } from '@/util/fetchVersions';
|
||||
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
|
||||
import { CmdK } from './CmdK';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{ readonly packageName: string; readonly version: string }>;
|
||||
readonly params: Promise<{
|
||||
readonly item?: string[] | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}>;
|
||||
}): Promise<Metadata> {
|
||||
const { packageName, version } = await params;
|
||||
|
||||
@@ -35,11 +43,22 @@ export async function generateMetadata({
|
||||
export default async function Layout({
|
||||
params,
|
||||
children,
|
||||
}: PropsWithChildren<{ readonly params: Promise<{ readonly packageName: string; readonly version: string }> }>) {
|
||||
const { packageName, version } = await params;
|
||||
}: PropsWithChildren<{
|
||||
readonly params: Promise<{
|
||||
readonly item?: string[] | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}>;
|
||||
}>) {
|
||||
const { packageName, version, item } = await params;
|
||||
|
||||
const versions = fetchVersions(packageName);
|
||||
|
||||
const hasEntryPoints = PACKAGES_WITH_ENTRY_POINTS.includes(packageName);
|
||||
|
||||
const entryPoints = hasEntryPoints ? fetchEntryPoints(packageName, version) : Promise.resolve([]);
|
||||
const { entryPoints: parsedEntrypoints } = parseDocsPathParams(item);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar closeButton={false} intent="inset">
|
||||
@@ -65,12 +84,13 @@ export default async function Layout({
|
||||
<PackageSelect />
|
||||
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
|
||||
<VersionSelect versionsPromise={versions} />
|
||||
{hasEntryPoints ? <EntryPointSelect entryPointsPromise={entryPoints} /> : null}
|
||||
<SearchButton />
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="bg-[#f3f3f4] p-0 py-4 pl-4 dark:bg-[#121214]">
|
||||
<Scrollbars>
|
||||
<Navigation packageName={packageName} version={version} />
|
||||
<Navigation entryPoint={parsedEntrypoints.join('.')} packageName={packageName} version={version} />
|
||||
</Scrollbars>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
@@ -0,0 +1,99 @@
|
||||
'use cache';
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||
import type { Metadata } from 'next';
|
||||
import { MDXRemote } from 'next-mdx-remote-client/rsc';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { DocItem } from '@/components/DocItem';
|
||||
import { fetchNode } from '@/util/fetchNode';
|
||||
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
|
||||
import { getSingletonHighlighter } from '@/util/shiki.bundle';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{
|
||||
readonly item?: string[] | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}>;
|
||||
}): Promise<Metadata> {
|
||||
const { item, packageName, version } = await params;
|
||||
|
||||
const { foundItem } = parseDocsPathParams(item);
|
||||
|
||||
if (!foundItem) {
|
||||
return {
|
||||
title: `${packageName} (${version})`,
|
||||
};
|
||||
}
|
||||
|
||||
const decodedItemName = decodeURIComponent(foundItem);
|
||||
const titlePart = decodedItemName.split(':')?.[0] ?? decodedItemName;
|
||||
|
||||
return {
|
||||
title: `${titlePart} (${packageName} - ${version})`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{
|
||||
readonly item?: string[] | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}>;
|
||||
}) {
|
||||
const { item, packageName, version } = await params;
|
||||
|
||||
const { entryPoints: parsedEntrypoints, foundItem } = parseDocsPathParams(item);
|
||||
|
||||
if (!foundItem) {
|
||||
const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
|
||||
|
||||
return (
|
||||
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
|
||||
<MDXRemote
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeShikiFromHighlighter,
|
||||
await getSingletonHighlighter({
|
||||
langs: ['typescript', 'javascript', 'shellscript'],
|
||||
themes: ['github-light', 'github-dark-dimmed'],
|
||||
}),
|
||||
{
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark-dimmed',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}}
|
||||
source={fileContent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const entryPointString = parsedEntrypoints.join('.');
|
||||
const node = await fetchNode({
|
||||
entryPoint: entryPointString,
|
||||
item: decodeURIComponent(foundItem),
|
||||
packageName,
|
||||
version,
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-screen-xl flex-col gap-8 px-6 py-4">
|
||||
<DocItem node={node} packageName={packageName} version={version} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
'use cache';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { DocItem } from '@/components/DocItem';
|
||||
import { fetchNode } from '@/util/fetchNode';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{
|
||||
readonly item: string;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}>;
|
||||
}): Promise<Metadata> {
|
||||
const { item, packageName, version } = await params;
|
||||
|
||||
const normalizeItem = item.split(encodeURIComponent(':'))[0];
|
||||
|
||||
return {
|
||||
title: `${normalizeItem} (${packageName} - ${version})`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
readonly params: Promise<{ readonly item: string; readonly packageName: string; readonly version: string }>;
|
||||
}) {
|
||||
const { item, packageName, version } = await params;
|
||||
|
||||
const node = await fetchNode({ item, packageName, version });
|
||||
|
||||
if (!node) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-screen-xl flex-col gap-8 px-6 py-4">
|
||||
<DocItem node={node} packageName={packageName} version={version} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
'use cache';
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||
import { MDXRemote } from 'next-mdx-remote-client/rsc';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { getSingletonHighlighter } from '@/util/shiki.bundle';
|
||||
|
||||
export default async function Page({ params }: { readonly params: Promise<{ readonly packageName: string }> }) {
|
||||
const { packageName } = await params;
|
||||
|
||||
const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
|
||||
|
||||
return (
|
||||
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
|
||||
<MDXRemote
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypeShikiFromHighlighter,
|
||||
await getSingletonHighlighter({
|
||||
langs: ['typescript', 'javascript', 'shellscript'],
|
||||
themes: ['github-light', 'github-dark-dimmed'],
|
||||
}),
|
||||
{
|
||||
themes: {
|
||||
light: 'github-light',
|
||||
dark: 'github-dark-dimmed',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}}
|
||||
source={fileContent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
'use cache';
|
||||
|
||||
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
|
||||
import { ConstructorNode } from './ConstructorNode';
|
||||
import { DeprecatedNode } from './DeprecatedNode';
|
||||
@@ -26,8 +28,6 @@ async function OverloadNode({
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}) {
|
||||
'use cache';
|
||||
|
||||
return (
|
||||
<Tabs className="flex flex-col gap-4">
|
||||
<TabList className="flex flex-wrap gap-2">
|
||||
@@ -63,8 +63,6 @@ export async function DocItem({
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}) {
|
||||
'use cache';
|
||||
|
||||
if (node.overloads?.length) {
|
||||
return <OverloadNode node={node} packageName={packageName} version={version} />;
|
||||
}
|
||||
|
||||
43
apps/website/src/components/EntrypointSelect.tsx
Normal file
43
apps/website/src/components/EntrypointSelect.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { use } from 'react';
|
||||
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
|
||||
import { Select, SelectList, SelectOption, SelectTrigger } from './ui/Select';
|
||||
|
||||
export function EntryPointSelect({
|
||||
entryPointsPromise,
|
||||
}: {
|
||||
readonly entryPointsPromise: Promise<{ readonly entryPoint: string }[]>;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const entryPoints = use(entryPointsPromise);
|
||||
|
||||
const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item as string[] | undefined);
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label="Select an entrypoint"
|
||||
defaultSelectedKey={parsedEntrypoints.length ? parsedEntrypoints.join('/') : 'global'}
|
||||
>
|
||||
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" />
|
||||
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={entryPoints}>
|
||||
{(item) => (
|
||||
<SelectOption
|
||||
className="dark:pressed:bg-[#313135] bg-[#f3f3f4] dark:bg-[#28282d] dark:hover:bg-[#313135]"
|
||||
href={`/docs/packages/${params.packageName}/${params.version}/${item.entryPoint}`}
|
||||
id={item.entryPoint || 'global'}
|
||||
key={item.entryPoint || 'global'}
|
||||
onHoverStart={() =>
|
||||
router.prefetch(`/docs/packages/${params.packageName}/${params.version}/${item.entryPoint}`)
|
||||
}
|
||||
textValue={item.entryPoint || 'global'}
|
||||
>
|
||||
{item.entryPoint || 'global'}
|
||||
</SelectOption>
|
||||
)}
|
||||
</SelectList>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,16 @@ import { resolveNodeKind } from './DocKind';
|
||||
import { NavigationItem } from './NavigationItem';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
|
||||
|
||||
export async function Navigation({ packageName, version }: { readonly packageName: string; readonly version: string }) {
|
||||
const node = await fetchSitemap({ packageName, version });
|
||||
export async function Navigation({
|
||||
entryPoint,
|
||||
packageName,
|
||||
version,
|
||||
}: {
|
||||
readonly entryPoint?: string | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}) {
|
||||
const node = await fetchSitemap({ entryPoint, packageName, version });
|
||||
|
||||
if (!node) {
|
||||
notFound();
|
||||
|
||||
@@ -11,7 +11,10 @@ export const PACKAGES = [
|
||||
{ name: 'util' },
|
||||
{ name: 'voice' },
|
||||
{ name: 'ws' },
|
||||
// { name: 'discord-api-types' },
|
||||
];
|
||||
|
||||
export const PACKAGES_WITH_ENTRY_POINTS = ['discord-api-types'];
|
||||
|
||||
export const DESCRIPTION =
|
||||
"discord.js is a powerful Node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.";
|
||||
|
||||
34
apps/website/src/util/fetchEntryPoints.ts
Normal file
34
apps/website/src/util/fetchEntryPoints.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
// import { sql } from '@vercel/postgres';
|
||||
import { ENV } from './env';
|
||||
|
||||
export async function fetchEntryPoints(packageName: string, version: string) {
|
||||
if (ENV.IS_LOCAL_DEV) {
|
||||
const fileContent = await readFile(
|
||||
join(process.cwd(), `../../packages/${packageName}/docs/${packageName}/split/${version}.entrypoints.api.json`),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return JSON.parse(fileContent);
|
||||
}
|
||||
|
||||
// try {
|
||||
// const { rows } = await sql<{
|
||||
// entryPoint: string;
|
||||
// }>`select entryPoint from documentation where name = ${packageName} and version = ${version} order by
|
||||
// case
|
||||
// when version = 'main' then 0
|
||||
// else 1
|
||||
// end,
|
||||
// case
|
||||
// when version = 'main' then null
|
||||
// else string_to_array(version, '.')::int[]
|
||||
// end desc;
|
||||
// `;
|
||||
|
||||
// return rows;
|
||||
// } catch {
|
||||
return [];
|
||||
// }
|
||||
}
|
||||
@@ -3,21 +3,24 @@ import { join } from 'node:path';
|
||||
import { ENV } from './env';
|
||||
|
||||
export async function fetchNode({
|
||||
entryPoint,
|
||||
item,
|
||||
packageName,
|
||||
version,
|
||||
}: {
|
||||
readonly entryPoint?: string | undefined;
|
||||
readonly item: any;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}) {
|
||||
const normalizeItem = item.split(encodeURIComponent(':')).join('.').toLowerCase();
|
||||
const normalizedEntryPoint = entryPoint ? `${entryPoint}.` : '';
|
||||
const normalizeItem = item.replaceAll(':', '.').toLowerCase();
|
||||
|
||||
if (ENV.IS_LOCAL_DEV) {
|
||||
const fileContent = await readFile(
|
||||
join(
|
||||
process.cwd(),
|
||||
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizeItem}.api.json`,
|
||||
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizedEntryPoint}${normalizeItem}.api.json`,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
@@ -27,7 +30,7 @@ export async function fetchNode({
|
||||
|
||||
const isMain = version === 'main';
|
||||
const fileContent = await fetch(
|
||||
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizeItem}.api.json`,
|
||||
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizedEntryPoint}${normalizeItem}.api.json`,
|
||||
{ next: { revalidate: isMain ? 0 : 604_800 } },
|
||||
);
|
||||
|
||||
|
||||
@@ -3,15 +3,22 @@ import { join } from 'node:path';
|
||||
import { ENV } from './env';
|
||||
|
||||
export async function fetchSitemap({
|
||||
entryPoint,
|
||||
packageName,
|
||||
version,
|
||||
}: {
|
||||
readonly entryPoint?: string | undefined;
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
}) {
|
||||
const normalizedEntryPoint = entryPoint ? `${entryPoint}.` : '';
|
||||
|
||||
if (ENV.IS_LOCAL_DEV) {
|
||||
const fileContent = await readFile(
|
||||
join(process.cwd(), `../../packages/${packageName}/docs/${packageName}/split/${version}.sitemap.api.json`),
|
||||
join(
|
||||
process.cwd(),
|
||||
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizedEntryPoint}sitemap.api.json`,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
@@ -20,7 +27,7 @@ export async function fetchSitemap({
|
||||
|
||||
const isMain = version === 'main';
|
||||
const fileContent = await fetch(
|
||||
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`,
|
||||
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizedEntryPoint}sitemap.api.json`,
|
||||
{
|
||||
next: { revalidate: isMain ? 0 : 604_800 },
|
||||
},
|
||||
|
||||
16
apps/website/src/util/parseDocsPathParams.ts
Normal file
16
apps/website/src/util/parseDocsPathParams.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function parseDocsPathParams(item: string[] | undefined): {
|
||||
entryPoints: string[];
|
||||
foundItem: string | undefined;
|
||||
} {
|
||||
if (!item?.length) {
|
||||
return { entryPoints: [], foundItem: undefined };
|
||||
}
|
||||
|
||||
const lastElement = item.at(-1);
|
||||
const hasTypeMarker = lastElement?.includes('%3A');
|
||||
|
||||
return {
|
||||
entryPoints: hasTypeMarker ? item.slice(0, -1) : item,
|
||||
foundItem: hasTypeMarker ? lastElement : undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user