feat(website): render syntax and mdx on the server (#9086)

This commit is contained in:
Suneet Tipirneni
2023-03-23 17:17:41 -04:00
committed by GitHub
parent bc641fa936
commit ee5169e0aa
91 changed files with 820 additions and 1688 deletions

View File

@@ -40,7 +40,7 @@ For example, to automatically recompile the `@discordjs/rest` project when chang
If you'd like to create another package under the `@discordjs` organization run the following command:
```bash
```sh
yarn create-package <package-name> [package-description]
```

View File

@@ -1,5 +1,5 @@
diff --git a/lib/TSDocConfigFile.js b/lib/TSDocConfigFile.js
index caf3515d60fd386c5909db5a0aa8b4180b10d602..6fa4f1984b6ba6b3a7aecd05e54477ebf141af94 100644
index caf3515d60fd386c5909db5a0aa8b4180b10d602..5f7cfed7611e3fe660b5265ff99c5da0beb7caec 100644
--- a/lib/TSDocConfigFile.js
+++ b/lib/TSDocConfigFile.js
@@ -31,8 +31,7 @@ const ajv_1 = __importDefault(require("ajv"));
@@ -8,7 +8,7 @@ index caf3515d60fd386c5909db5a0aa8b4180b10d602..6fa4f1984b6ba6b3a7aecd05e54477eb
function initializeSchemaValidator() {
- const jsonSchemaPath = resolve.sync('@microsoft/tsdoc/schemas/tsdoc.schema.json', { basedir: __dirname });
- const jsonSchemaContent = fs.readFileSync(jsonSchemaPath).toString();
+ const jsonSchemaContent = "{\"title\":\"TSDoc Configuration\",\"description\":\"Describes the TSDoc configuration for a TypeScript project\",\"type\":\"object\",\"properties\":{\"$schema\":{\"description\":\"Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.\",\"type\":\"string\"},\"extends\":{\"description\":\"Optionally specifies one or more JSON config files that will be combined with this file. This provides a way for standard settings to be shared across multiple projects. Important: The \\\"extends\\\" paths are resolved using NodeJS module resolution, so a path to a local file MUST be prefixed with \\\"./\\\".\",\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"noStandardTags\":{\"description\":\"By default, the config file loader will predefine all of the standardized TSDoc tags. To disable this and start with a completely empty configuration, set \\\"noStandardTags\\\"=true.\",\"type\":\"boolean\"},\"tagDefinitions\":{\"description\":\"Additional tags to support when parsing documentation comments with TSDoc.\",\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/tsdocTagDefinition\"}},\"supportedHtmlElements\":{\"description\":\"The HTML element names that are supported in this configuration. Used in conjunction with the \\\"reportUnsupportedHtmlElements\\\" setting.\",\"type\":\"array\",\"items\":{\"type\":\"string\",\"pattern\":\"^[a-zA-Z0-9-]+$\"}},\"reportUnsupportedHtmlElements\":{\"description\":\"Whether an error should be reported when an unsupported HTML element is encountered in a doc comment. Defaults to \\\"true\\\" if the \\\"supportedHtmlElements\\\" field is present in this file, \\\"false\\\" if not.\",\"type\":\"boolean\"},\"supportForTags\":{\"description\":\"A collection of key/value pairs. The key is a TSDoc tag name (e.g. \\\"@myTag\\\") that must be defined in this configuration. The value is a boolean indicating whether the tag is supported. The TSDoc parser may report warnings when unsupported tags are encountered. If \\\"supportForTags\\\" is specified for at least one tag, then the \\\"reportUnsupportedTags\\\" validation check is enabled by default.\",\"type\":\"object\",\"patternProperties\":{\"@[a-zA-Z][a-zA-Z0-9]*$\":{\"type\":\"boolean\"}},\"additionalItems\":false}},\"required\":[\"$schema\"],\"additionalProperties\":false,\"definitions\":{\"tsdocTagDefinition\":{\"description\":\"Configuration for a custom supported TSDoc tag.\",\"type\":\"object\",\"properties\":{\"tagName\":{\"description\":\"Name of the custom tag. TSDoc tag names start with an at-sign (@) followed by ASCII letters using camelCase capitalization.\",\"type\":\"string\"},\"syntaxKind\":{\"description\":\"Syntax kind of the custom tag. \\\"inline\\\" means that this tag can appear inside other documentation sections (example: {@link}). \\\"block\\\" means that this tag starts a new documentation section (example: @remarks). \\\"modifier\\\" means that this tag's presence indicates an aspect of the associated API item (example: @internal).\",\"type\":\"string\",\"enum\":[\"inline\",\"block\",\"modifier\"]},\"allowMultiple\":{\"description\":\"If true, then this tag may appear multiple times in a doc comment. By default, a tag may only appear once.\",\"type\":\"boolean\"}},\"required\":[\"tagName\",\"syntaxKind\"],\"additionalProperties\":false}}}";
+ const jsonSchemaContent = '{\"title\":\"TSDoc Configuration\",\"description\":\"Describes the TSDoc configuration for a TypeScript project\",\"type\":\"object\",\"properties\":{\"$schema\":{\"description\":\"Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.\",\"type\":\"string\"},\"extends\":{\"description\":\"Optionally specifies one or more JSON config files that will be combined with this file. This provides a way for standard settings to be shared across multiple projects. Important: The \\\"extends\\\" paths are resolved using NodeJS module resolution, so a path to a local file MUST be prefixed with \\\".\/\\\".\",\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"noStandardTags\":{\"description\":\"By default, the config file loader will predefine all of the standardized TSDoc tags. To disable this and start with a completely empty configuration, set \\\"noStandardTags\\\"=true.\",\"type\":\"boolean\"},\"tagDefinitions\":{\"description\":\"Additional tags to support when parsing documentation comments with TSDoc.\",\"type\":\"array\",\"items\":{\"$ref\":\"#\/definitions\/tsdocTagDefinition\"}},\"supportedHtmlElements\":{\"description\":\"The HTML element names that are supported in this configuration. Used in conjunction with the \\\"reportUnsupportedHtmlElements\\\" setting.\",\"type\":\"array\",\"items\":{\"type\":\"string\",\"pattern\":\"^[a-zA-Z0-9-]+$\"}},\"reportUnsupportedHtmlElements\":{\"description\":\"Whether an error should be reported when an unsupported HTML element is encountered in a doc comment. Defaults to \\\"true\\\" if the \\\"supportedHtmlElements\\\" field is present in this file, \\\"false\\\" if not.\",\"type\":\"boolean\"},\"supportForTags\":{\"description\":\"A collection of key\/value pairs. The key is a TSDoc tag name (e.g. \\\"@myTag\\\") that must be defined in this configuration. The value is a boolean indicating whether the tag is supported. The TSDoc parser may report warnings when unsupported tags are encountered. If \\\"supportForTags\\\" is specified for at least one tag, then the \\\"reportUnsupportedTags\\\" validation check is enabled by default.\",\"type\":\"object\",\"patternProperties\":{\"@[a-zA-Z][a-zA-Z0-9]*$\":{\"type\":\"boolean\"}},\"additionalItems\":false}},\"required\":[\"$schema\"],\"additionalProperties\":false,\"definitions\":{\"tsdocTagDefinition\":{\"description\":\"Configuration for a custom supported TSDoc tag.\",\"type\":\"object\",\"properties\":{\"tagName\":{\"description\":\"Name of the custom tag. TSDoc tag names start with an at-sign (@) followed by ASCII letters using camelCase capitalization.\",\"type\":\"string\"},\"syntaxKind\":{\"description\":\"Syntax kind of the custom tag. \\\"inline\\\" means that this tag can appear inside other documentation sections (example: {@link}). \\\"block\\\" means that this tag starts a new documentation section (example: @remarks). \\\"modifier\\\" means that this tag\'s presence indicates an aspect of the associated API item (example: @internal).\",\"type\":\"string\",\"enum\":[\"inline\",\"block\",\"modifier\"]},\"allowMultiple\":{\"description\":\"If true, then this tag may appear multiple times in a doc comment. By default, a tag may only appear once.\",\"type\":\"boolean\"}},\"required\":[\"tagName\",\"syntaxKind\"],\"additionalProperties\":false}}}';
const jsonSchema = jju.parse(jsonSchemaContent, { mode: 'cjson' });
return ajv.compile(jsonSchema);
}

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
yarnPath: .yarn/releases/yarn-3.4.1.cjs
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@@ -30,7 +30,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js
@@ -48,7 +48,7 @@ pnpm add discord.js
Install discord.js:
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js

View File

@@ -92,7 +92,6 @@ export default defineConfig({
compress(),
],
markdown: {
extendDefaultPlugins: true,
syntaxHighlight: false,
},
vite: {

View File

@@ -60,7 +60,7 @@
"@unocss/reset": "^0.50.6",
"@vitejs/plugin-react": "^3.1.0",
"@vitest/coverage-c8": "^0.29.7",
"astro": "^1.9.2",
"astro": "^2.1.5",
"astro-compress": "^1.1.35",
"astro-critters": "^1.1.31",
"cross-env": "^7.0.3",
@@ -80,7 +80,7 @@
"shiki": "^0.14.1",
"typescript": "^5.0.2",
"unocss": "^0.50.6",
"vercel": "^28.18.0",
"vercel": "^28.18.1",
"vitest": "^0.29.7"
},
"engines": {

View File

@@ -28,7 +28,7 @@ export function SidebarItems({ pages }: { pages: MDXPage[] }) {
}, [state]);
return Object.keys(categories).map((category, idx) => (
<Section key={idx} title={category}>
<Section key={`${category}-${idx}`} title={category}>
{categories[category]?.map((member, index) => (
<a
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l p-[5px] pl-6 outline-0 focus:rounded focus:border-0 focus:ring ${
@@ -38,7 +38,7 @@ export function SidebarItems({ pages }: { pages: MDXPage[] }) {
}`}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
href={member.url || '/'}
key={index}
key={`${member.frontmatter.title}-${index}}`}
title={member.frontmatter.title}
>
<div className="flex flex-row place-items-center gap-2 lg:text-sm">

View File

@@ -28,7 +28,6 @@ const curCategoryPages = groupedPages[category];
const curCategoryIndex = curCategoryPages!.findIndex((page) => page.url === url);
const pagePrev = curCategoryPages![curCategoryIndex - 1];
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const pageNext = curCategoryPages![curCategoryIndex + 1];
---

2
apps/guide/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference types="@astrojs/image/client" />

View File

@@ -26,7 +26,7 @@ const props = Astro.props;
<meta content="website" property="og:type" />
<meta content="discord.js guide" property="og:title" />
<meta content={DESCRIPTION} name="og:description" />
<meta content="https://discordjs.dev/open-graph.png" property="og:image" />
<meta content="https://discordjs.dev/api/open-graph.png" property="og:image" />
<meta content="summary_large_image" name="twitter:card" />
<meta content="@iCrawlToGo" name="twitter:creator" />

View File

@@ -78,15 +78,15 @@ You can use the [_`dotenv`_ package](https://www.npmjs.com/package/dotenv) for t
<CH.Code client:load>
```shellscript npm
```sh npm
npm install dotenv
```
```shellscript yarn
```sh yarn
yarn add dotenv
```
```shellscript pnpm
```sh pnpm
pnpm add dotenv
```

View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -1,4 +1,3 @@
/* eslint-disable tsdoc/syntax */
import { fileURLToPath } from 'node:url';
import bundleAnalyzer from '@next/bundle-analyzer';
@@ -6,9 +5,6 @@ const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
/**
* @type {import('next').NextConfig}
*/
export default withBundleAnalyzer({
reactStrictMode: true,
eslint: {
@@ -18,16 +14,15 @@ export default withBundleAnalyzer({
typescript: {
ignoreBuildErrors: true,
},
cleanDistDir: true,
outputFileTracing: true,
experimental: {
appDir: true,
serverComponentsExternalPackages: ['@microsoft/api-extractor-model', 'jju', 'shiki'],
outputFileTracingRoot: fileURLToPath(new URL('../../', import.meta.url)),
fallbackNodePolyfills: false,
serverComponentsExternalPackages: ['@microsoft/api-extractor-model', 'jju'],
},
images: {
dangerouslyAllowSVG: true,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
contentSecurityPolicy: "default-src 'self'; script-src 'none'; frame-src 'none'; sandbox;",
},
});

View File

@@ -52,23 +52,21 @@
"@vercel/og": "^0.4.1",
"@vscode/codicons": "^0.0.32",
"ariakit": "^2.0.0-next.43",
"bright": "^0.7.0",
"cmdk": "^0.2.0",
"meilisearch": "^0.31.1",
"next": "^13.2.4",
"next": "^13.2.5-canary.15",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-custom-scrollbars-2": "^4.5.0",
"react-dom": "^18.2.0",
"react-syntax-highlighter": "^15.5.0",
"react-use": "^17.4.0",
"rehype-ignore": "^1.0.4",
"rehype-pretty-code": "^0.9.4",
"rehype-raw": "^6.1.1",
"rehype-slug": "^5.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.3",
"shiki": "^0.14.1",
"swr": "^2.1.1"
},
"devDependencies": {
@@ -78,7 +76,6 @@
"@types/node": "16.18.18",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@unocss/cli": "^0.50.6",
"@unocss/reset": "^0.50.6",
"@vitejs/plugin-react": "^3.1.0",
@@ -94,7 +91,7 @@
"prettier-plugin-tailwindcss": "^0.2.5",
"typescript": "^5.0.2",
"unocss": "^0.50.6",
"vercel": "^28.18.0",
"vercel": "^28.18.1",
"vitest": "^0.29.7"
},
"engines": {

View File

@@ -1,11 +1,15 @@
/* eslint-disable react/no-unknown-property */
import type { ApiItemKind } from '@microsoft/api-extractor-model';
import { ImageResponse } from '@vercel/og';
import type { NextRequest } from 'next/server';
import type { ServerRuntime } from 'next/types';
export const runtime: ServerRuntime = 'edge';
const fonts = Promise.all([
fetch(new URL('../../assets/fonts/Inter-Regular.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../assets/fonts/Inter-Bold.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../../assets/fonts/Inter-Regular.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../../assets/fonts/Inter-Bold.ttf', import.meta.url)).then(async (res) => res.arrayBuffer()),
]);
function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
@@ -20,9 +24,9 @@ function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clip-rule="evenodd"
clipRule="evenodd"
d="M14 2H8L7 3v3h1V3h6v5h-4v1h4l1-1V3l-1-1zM9 6h4v1H9.41L9 6.59V6zM7 7H2L1 8v5l1 1h6l1-1V8L8 7H7zm1 6H2V8h6v5zM3 9h4v1H3V9zm0 2h4v1H3v-1zm6-7h4v1H9V4z"
fill-rule="evenodd"
fillRule="evenodd"
/>
</svg>
);
@@ -30,9 +34,9 @@ function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clip-rule="evenodd"
clipRule="evenodd"
d="M7 3l1-1h6l1 1v5l-1 1h-4V8h4V3H8v3H7V3zm2 6V8L8 7H2L1 8v5l1 1h6l1-1V9zM8 8v5H2V8h6zm1.414-1L9 6.586V6h4v1H9.414zM9 4h4v1H9V4zm-2 6H3v1h4v-1z"
fill-rule="evenodd"
fillRule="evenodd"
/>
</svg>
);
@@ -52,9 +56,9 @@ function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
return (
<svg fill="white" height={size} viewBox="0 0 16 16" width={size} xmlns="http://www.w3.org/2000/svg">
<path
clip-rule="evenodd"
clipRule="evenodd"
d="M2 5h2V4H1.5l-.5.5v8l.5.5H4v-1H2V5zm12.5-1H12v1h2v7h-2v1h2.5l.5-.5v-8l-.5-.5zm-2.74 2.57L12 7v2.51l-.3.45-4.5 2h-.46l-2.5-1.5-.24-.43v-2.5l.3-.46 4.5-2h.46l2.5 1.5zM5 9.71l1.5.9V9.28L5 8.38v1.33zm.58-2.15l1.45.87 3.39-1.5-1.45-.87-3.39 1.5zm1.95 3.17l3.5-1.56v-1.4l-3.5 1.55v1.41z"
fill-rule="evenodd"
fillRule="evenodd"
/>
</svg>
);
@@ -73,10 +77,10 @@ function resolveIcon(icon: keyof typeof ApiItemKind, size = 88) {
}
}
export default async function handler(req: NextRequest) {
export async function GET(request: NextRequest) {
const fontData = await fonts;
const { searchParams } = new URL(req.url);
const { searchParams } = new URL(request.url);
const hasPkg = searchParams.has('pkg');
const hasKind = searchParams.has('kind');
@@ -164,7 +168,3 @@ export default async function handler(req: NextRequest) {
},
);
}
export const config = {
runtime: 'experimental-edge',
};

View File

@@ -1,11 +1,15 @@
/* eslint-disable react/no-unknown-property */
import { ImageResponse } from '@vercel/og';
const fonts = fetch(new URL('../../assets/fonts/Inter-Black.ttf', import.meta.url)).then(async (res) =>
import { ImageResponse } from '@vercel/og';
import type { ServerRuntime } from 'next/types';
export const runtime: ServerRuntime = 'edge';
const fonts = fetch(new URL('../../../assets/fonts/Inter-Black.ttf', import.meta.url)).then(async (res) =>
res.arrayBuffer(),
);
export default async function handler() {
export async function GET() {
const fontData = await fonts;
return new ImageResponse(
@@ -38,7 +42,3 @@ export default async function handler() {
},
);
}
export const config = {
runtime: 'experimental-edge',
};

View File

@@ -15,7 +15,13 @@ export async function fetchModelJSON(packageName: string, version: string): Prom
join(process.cwd(), '..', '..', 'packages', packageName, 'docs', 'docs.api.json'),
'utf8',
);
return JSON.parse(res);
try {
return JSON.parse(res);
} catch {
console.log(res);
return {};
}
}
const response = await fetch(`https://docs.discordjs.dev/docs/${packageName}/${version}.api.json`, {

View File

@@ -1,12 +0,0 @@
import type { PropsWithChildren } from 'react';
import { Providers } from './providers';
import { CmdKDialog } from '~/components/CmdK';
export default function ItemLayout({ children }: PropsWithChildren) {
return (
<Providers>
<>{children}</>
<CmdKDialog />
</Providers>
);
}

View File

@@ -1,9 +1,4 @@
/* eslint-disable no-case-declarations */
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
// eslint-disable-next-line n/prefer-global/process
import process, { cwd } from 'node:process';
import { createApiModel, tryResolveSummaryText } from '@discordjs/scripts';
import { addPackageToModel, tryResolveSummaryText } from '@discordjs/scripts';
import type {
ApiClass,
ApiDeclaredItem,
@@ -18,9 +13,9 @@ import type {
ApiTypeAlias,
ApiVariable,
} from '@microsoft/api-extractor-model';
import { ApiItemKind, ApiFunction } from '@microsoft/api-extractor-model';
import type { Metadata } from 'next';
import { ApiItemKind, ApiModel, ApiFunction } from '@microsoft/api-extractor-model';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next/types';
import { fetchModelJSON } from '~/app/docAPI';
import { Class } from '~/components/model/Class';
import { Interface } from '~/components/model/Interface';
@@ -39,7 +34,7 @@ export interface ItemRouteParams {
async function fetchHeadMember({ package: packageName, version, item }: ItemRouteParams): Promise<ApiItem | undefined> {
const modelJSON = await fetchModelJSON(packageName, version);
const model = createApiModel(modelJSON);
const model = addPackageToModel(new ApiModel(), modelJSON);
const pkg = model.tryGetPackageByName(packageName);
const entry = pkg?.entryPoints[0];
@@ -89,11 +84,11 @@ function resolveMemberSearchParams(packageName: string, member: ApiItem): URLSea
return params;
}
export async function generateMetadata({ params }: { params: ItemRouteParams }): Promise<Metadata> {
export async function generateMetadata({ params }: { params: ItemRouteParams }) {
const member = (await fetchHeadMember(params))!;
const name = `discord.js${member?.displayName ? ` | ${member.displayName}` : ''}`;
const ogTitle = `${params.package ?? 'discord.js'}${member?.displayName ? ` | ${member.displayName}` : ''}`;
const url = new URL('https://discordjs.dev/api/og_model');
const url = new URL('https://discordjs.dev/api/dynamic-open-graph.png');
const searchParams = resolveMemberSearchParams(params.package, member);
url.search = searchParams.toString();
const ogImage = url.toString();
@@ -107,21 +102,21 @@ export async function generateMetadata({ params }: { params: ItemRouteParams }):
description: description ?? 'Discord.js API Documentation',
images: ogImage,
},
};
} satisfies Metadata;
}
export async function generateStaticParams({ params: { package: packageName, version } }: { params: ItemRouteParams }) {
const modelJSON = await fetchModelJSON(packageName, version);
const model = createApiModel(modelJSON);
const model = addPackageToModel(new ApiModel(), modelJSON);
const pkg = model.tryGetPackageByName(packageName);
const entry = pkg?.entryPoints[0];
if (!entry) {
return notFound();
notFound();
}
return entry.members.map((member) => ({
return entry.members.map((member: ApiItem) => ({
item: member.displayName,
}));
}
@@ -131,21 +126,20 @@ async function fetchMember({ package: packageName, version: branchName = 'main',
notFound();
}
let data;
try {
if (process.env.NEXT_PUBLIC_LOCAL_DEV) {
const res = await readFile(join(cwd(), '..', '..', 'packages', packageName, 'docs', 'docs.api.json'), 'utf8');
data = JSON.parse(res);
} else {
const res = await fetch(`https://docs.discordjs.dev/docs/${packageName}/${branchName}.api.json`);
data = await res.json();
const model = new ApiModel();
if (branchName === 'main') {
const modelJSONFiles = await Promise.all(PACKAGES.map(async (pkg) => fetchModelJSON(pkg, branchName)));
for (const modelJSONFile of modelJSONFiles) {
addPackageToModel(model, modelJSONFile);
}
} catch {
notFound();
} else {
const modelJSON = await fetchModelJSON(packageName, branchName);
addPackageToModel(model, modelJSON);
}
const [memberName, overloadIndex] = decodeURIComponent(item).split(OVERLOAD_SEPARATOR);
const model = createApiModel(data);
// eslint-disable-next-line prefer-const
let { containerKey, displayName: name } = findMember(model, packageName, memberName) ?? {};
@@ -161,7 +155,7 @@ function Member({ member }: { member?: ApiItem }) {
case 'Class':
return <Class clazz={member as ApiClass} />;
case 'Function':
return <Function item={member as ApiFunction} key={member.containerKey} />;
return <Function item={member as ApiFunction} key={`${member.displayName}-${member.containerKey}`} />;
case 'Interface':
return <Interface item={member as ApiInterface} />;
case 'TypeAlias':
@@ -179,7 +173,7 @@ export default async function Page({ params }: { params: ItemRouteParams }) {
const member = await fetchMember(params);
return (
<main
<div
className={
(member?.kind === 'Class' || member?.kind === 'Interface') && (member as ApiClass | ApiInterface).members.length
? 'xl:pr-64'
@@ -189,6 +183,6 @@ export default async function Page({ params }: { params: ItemRouteParams }) {
<article className="dark:bg-dark-600 bg-light-600">
<div className="dark:bg-dark-800 bg-white p-6 pb-20 shadow">{member ? <Member member={member} /> : null}</div>
</article>
</main>
</div>
);
}

View File

@@ -1,27 +0,0 @@
'use client';
// import { ThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';
import { CmdKProvider } from '~/contexts/cmdK';
import { NavProvider } from '~/contexts/nav';
export function Providers({ children }: PropsWithChildren) {
return (
<NavProvider>
<CmdKProvider>
{/* <ThemeProvider
attribute="class"
cookieName="theme"
defaultTheme="system"
disableTransitionOnChange
value={{
light: 'light',
dark: 'dark',
}}
> */}
{children}
{/* </ThemeProvider> */}
</CmdKProvider>
</NavProvider>
);
}

View File

@@ -0,0 +1,12 @@
'use client';
export default function Error({ error }: { error: Error }) {
console.error(error);
return (
<div className="mx-auto flex h-full max-w-lg flex-col place-content-center place-items-center gap-8 py-16 px-8 lg:py-0 lg:px-6">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
);
}

View File

@@ -1,10 +1,13 @@
import { createApiModel } from '@discordjs/scripts';
import { addPackageToModel } from '@discordjs/scripts';
import type { ApiFunction, ApiItem } from '@microsoft/api-extractor-model';
import { ApiModel } from '@microsoft/api-extractor-model';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { Providers } from './providers';
import { fetchModelJSON, fetchVersions } from '~/app/docAPI';
import vercelLogo from '~/assets/powered-by-vercel.svg';
import { CmdKDialog } from '~/components/CmdK';
import { Header } from '~/components/Header';
import { Nav } from '~/components/Nav';
import type { SidebarSectionItemData } from '~/components/Sidebar';
@@ -41,18 +44,18 @@ function serializeIntoSidebarItemData(item: ApiItem): SidebarSectionItemData {
export default async function PackageLayout({ children, params }: PropsWithChildren<{ params: VersionRouteParams }>) {
const modelJSON = await fetchModelJSON(params.package, params.version);
const model = createApiModel(modelJSON);
const model = addPackageToModel(new ApiModel(), modelJSON);
const pkg = model.tryGetPackageByName(params.package);
if (!pkg) {
return notFound();
notFound();
}
const entry = pkg.entryPoints[0];
if (!entry) {
return notFound();
notFound();
}
const members = entry.members.filter((member) => {
@@ -64,80 +67,83 @@ export default async function PackageLayout({ children, params }: PropsWithChild
});
return (
<>
<Header />
<Nav members={members.map((member) => serializeIntoSidebarItemData(member))} />
<article className="pt-18 lg:pl-76">
<div className="relative z-10 min-h-[calc(100vh_-_70px)]">{children}</div>
<div className="h-76 md:h-52" />
<footer className="dark:bg-dark-600 h-76 lg:pl-84 bg-light-600 fixed bottom-0 left-0 right-0 md:h-52 md:pl-4 md:pr-16">
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 pt-12 lg:place-content-center">
<div className="flex w-full flex-col place-content-between place-items-center gap-12 md:flex-row md:gap-0">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="noopener noreferrer"
target="_blank"
title="Vercel"
>
<Image alt="Vercel" src={vercelLogo} />
</a>
<div className="flex flex-row gap-6 md:gap-12">
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Community</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord.gg/djs"
rel="noopener noreferrer"
target="_blank"
>
Discord
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js/discussions"
rel="noopener noreferrer"
target="_blank"
>
GitHub discussions
</a>
<Providers>
<main>
<Header />
<Nav members={members.map((member) => serializeIntoSidebarItemData(member))} />
<article className="pt-18 lg:pl-76">
<div className="relative z-10 min-h-[calc(100vh_-_70px)]">{children}</div>
<div className="h-76 md:h-52" />
<footer className="dark:bg-dark-600 h-76 lg:pl-84 bg-light-600 fixed bottom-0 left-0 right-0 md:h-52 md:pl-4 md:pr-16">
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 pt-12 lg:place-content-center">
<div className="flex w-full flex-col place-content-between place-items-center gap-12 md:flex-row md:gap-0">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="noopener noreferrer"
target="_blank"
title="Vercel"
>
<Image alt="Vercel" src={vercelLogo} />
</a>
<div className="flex flex-row gap-6 md:gap-12">
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Community</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord.gg/djs"
rel="noopener noreferrer"
target="_blank"
>
Discord
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js/discussions"
rel="noopener noreferrer"
target="_blank"
>
GitHub discussions
</a>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Project</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js"
rel="noopener noreferrer"
target="_blank"
>
discord.js
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discordjs.guide"
rel="noopener noreferrer"
target="_blank"
>
discord.js guide
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord-api-types.dev"
rel="noopener noreferrer"
target="_blank"
>
discord-api-types
</a>
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold">Project</div>
<div className="flex flex-col gap-1">
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://github.com/discordjs/discord.js"
rel="noopener noreferrer"
target="_blank"
>
discord.js
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discordjs.guide"
rel="noopener noreferrer"
target="_blank"
>
discord.js guide
</a>
<a
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 focus:ring"
href="https://discord-api-types.dev"
rel="noopener noreferrer"
target="_blank"
>
discord-api-types
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
</article>
</>
</footer>
</article>
</main>
<CmdKDialog />
</Providers>
);
}

View File

@@ -1,66 +1,36 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { serialize } from 'next-mdx-remote/serialize';
import type { SerializeOptions } from 'next-mdx-remote/dist/types';
import { MDXRemote } from 'next-mdx-remote/rsc';
import rehypeIgnore from 'rehype-ignore';
import rehypePrettyCode, { type Options } from 'rehype-pretty-code';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { getHighlighter } from 'shiki';
import shikiLangJavascript from 'shiki/languages/javascript.tmLanguage.json';
import shikiLangTypescript from 'shiki/languages/typescript.tmLanguage.json';
import shikiThemeDarkPlus from 'shiki/themes/dark-plus.json';
import shikiThemeLightPlus from 'shiki/themes/light-plus.json';
import type { VersionRouteParams } from './layout';
import { MDXRemote } from '~/components/MDXRemote';
import { SyntaxHighlighter } from '~/components/SyntaxHighlighter';
async function loadREADME(packageName: string) {
return readFile(join(process.cwd(), 'src', 'assets', 'readme', packageName, 'home-README.md'), 'utf8');
}
async function generateMDX(readme: string) {
return serialize(readme, {
mdxOptions: {
remarkPlugins: [remarkGfm],
remarkRehypeOptions: { allowDangerousHtml: true },
rehypePlugins: [
rehypeRaw,
rehypeIgnore,
rehypeSlug,
[
rehypePrettyCode,
{
theme: {
dark: shikiThemeDarkPlus,
light: shikiThemeLightPlus,
},
getHighlighter: async (options?: Partial<Options>) =>
getHighlighter({
...options,
langs: [
// @ts-expect-error: Working as intended
{ id: 'javascript', aliases: ['js'], scopeName: 'source.js', grammar: shikiLangJavascript },
// @ts-expect-error: Working as intended
{ id: 'typescript', aliases: ['ts'], scopeName: 'source.ts', grammar: shikiLangTypescript },
],
}),
},
],
],
format: 'md',
},
});
}
const mdxOptions = {
mdxOptions: {
remarkPlugins: [remarkGfm],
remarkRehypeOptions: { allowDangerousHtml: true },
rehypePlugins: [rehypeRaw, rehypeIgnore, rehypeSlug],
format: 'md',
},
} satisfies SerializeOptions;
export default async function Page({ params }: { params: VersionRouteParams }) {
const { package: packageName } = params;
const readmeSource = await loadREADME(packageName);
const mdxSource = await generateMDX(readmeSource);
return (
<article className="dark:bg-dark-600 bg-white p-10">
<div className="prose max-w-none">
<MDXRemote {...mdxSource} />
{/* @ts-expect-error async component */}
<MDXRemote components={{ pre: SyntaxHighlighter }} options={mdxOptions} source={readmeSource} />
</div>
</article>
);

View File

@@ -0,0 +1,13 @@
'use client';
import type { PropsWithChildren } from 'react';
import { CmdKProvider } from '~/contexts/cmdK';
import { NavProvider } from '~/contexts/nav';
export function Providers({ children }: PropsWithChildren) {
return (
<NavProvider>
<CmdKProvider>{children}</CmdKProvider>
</NavProvider>
);
}

View File

@@ -24,14 +24,14 @@ export default async function Page({ params }: { params: { package: string } })
const data = await getData(params.package);
return (
<div className="min-w-xs sm:w-md mx-auto flex h-full flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
<div className="min-w-xs sm:w-md mx-auto flex h-screen flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
<div className="flex grow flex-col place-content-center gap-4">
<h1 className="text-2xl font-semibold">Select a version:</h1>
{data.map((version) => (
{data.map((version, idx) => (
<Link
className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 focus:ring-width-2 focus:ring-blurple flex h-11 transform-gpu cursor-pointer select-none appearance-none flex-col place-content-center rounded border border-neutral-300 bg-transparent p-4 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 focus:ring active:translate-y-px active:bg-neutral-200 dark:text-white"
href={`/docs/packages/${params.package}/${version}`}
key={version}
key={`${version}-${idx}`}
>
<div className="flex flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-content-between place-items-center gap-4">

View File

@@ -5,7 +5,7 @@ import { VscPackage } from '@react-icons/all-files/vsc/VscPackage';
import Link from 'next/link';
import { PACKAGES } from '~/util/constants';
export default async function Page() {
export default function Page() {
return (
<div className="min-w-xs sm:w-md mx-auto flex min-h-screen flex-row place-content-center place-items-center gap-8 py-0 px-4 lg:py-0 lg:px-6">
<div className="flex grow flex-col place-content-center gap-4">
@@ -24,11 +24,11 @@ export default async function Page() {
<VscArrowRight size={20} />
</div>
</a>
{PACKAGES.map((pkg) => (
{PACKAGES.map((pkg, idx) => (
<Link
className="dark:bg-dark-400 dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 focus:ring-width-2 focus:ring-blurple flex h-11 transform-gpu cursor-pointer select-none appearance-none flex-row place-content-between rounded border border-neutral-300 bg-transparent p-4 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 focus:ring active:translate-y-px active:bg-neutral-200 dark:text-white"
href={`/docs/packages/${pkg}`}
key={pkg}
key={`${pkg}-${idx}`}
>
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex grow flex-row place-content-between place-items-center gap-4">

View File

@@ -0,0 +1,12 @@
'use client';
export default function Error({ error }: { error: Error }) {
console.error(error);
return (
<div className="mx-auto flex h-full max-w-lg flex-col place-content-center place-items-center gap-8 py-16 px-8 lg:py-0 lg:px-6">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { Providers } from './providers';
import { inter } from '~/util/fonts';
export default function GlobalError({ error }: { error: Error }) {
console.error(error);
return (
<html className={inter.variable} lang="en" suppressHydrationWarning>
<body className="dark:bg-dark-800 bg-white">
<Providers>
<main className="mx-auto h-screen max-w-2xl">
<div className="mx-auto flex h-full max-w-lg flex-col place-content-center place-items-center gap-8 py-16 px-8 lg:py-0 lg:px-6">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">500</h1>
<h2 className="text-[2rem] md:text-[3rem]">Error.</h2>
</div>
</main>
</Providers>
</body>
</html>
);
}

View File

@@ -1,15 +1,18 @@
import type { PropsWithChildren } from 'react';
import { Providers } from './providers';
import { inter } from '~/util/fonts';
import '@unocss/reset/tailwind.css';
import '../styles/inter.css';
import '@unocss/reset/tailwind-compat.css';
import '../styles/unocss.css';
import '../styles/cmdk.css';
import '../styles/main.css';
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body className="dark:bg-dark-800 bg-white">{children}</body>
<html className={inter.variable} lang="en" suppressHydrationWarning>
<body className="dark:bg-dark-800 bg-white">
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@@ -1,11 +1,12 @@
import { FiExternalLink } from '@react-icons/all-files/fi/FiExternalLink';
import Image from 'next/image';
import Link from 'next/link';
import type { Metadata } from 'next/types';
import vercelLogo from '../assets/powered-by-vercel.svg';
import { SyntaxHighlighter } from '~/components/SyntaxHighlighter';
import { CODE_EXAMPLE, DESCRIPTION } from '~/util/constants';
export const metadata = {
export const metadata: Metadata = {
title: 'discord.js',
description: DESCRIPTION,
viewport: {
@@ -51,7 +52,7 @@ export const metadata = {
type: 'website',
title: 'discord.js',
description: DESCRIPTION,
images: 'https://discordjs.dev/api/og',
images: 'https://discordjs.dev/api/open-graph.png',
},
twitter: {
@@ -66,7 +67,7 @@ export const metadata = {
export default function Page() {
return (
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 py-16 px-8 lg:h-full lg:place-content-center lg:py-0 lg:px-6">
<div className="mx-auto flex h-screen max-w-6xl flex-col place-items-center gap-12 py-16 px-8 lg:place-content-center lg:py-0 lg:px-6">
<div className="flex flex-col place-items-center gap-10 lg:flex-row lg:gap-6">
<div className="flex max-w-lg flex-col gap-3 lg:mr-8">
<h1 className="text-3xl font-black leading-tight sm:text-5xl sm:leading-tight">
@@ -104,7 +105,10 @@ export default function Page() {
</a>
</div>
</div>
<SyntaxHighlighter code={CODE_EXAMPLE} />
<div className="max-w-sm sm:max-w-6xl">
{/* @ts-expect-error async component */}
<SyntaxHighlighter code={CODE_EXAMPLE} />
</div>
</div>
<div className="flex flex-row place-content-center">
<a

View File

@@ -0,0 +1,8 @@
'use client';
import { ThemeProvider } from 'next-themes';
import type { PropsWithChildren } from 'react';
export function Providers({ children }: PropsWithChildren) {
return <ThemeProvider attribute="class">{children}</ThemeProvider>;
}

Binary file not shown.

View File

@@ -48,10 +48,10 @@ export function CmdKDialog() {
const searchResultItems = useMemo(
() =>
searchResults?.map((item) => (
searchResults?.map((item, idx) => (
<Command.Item
className="dark:border-dark-100 dark:hover:bg-dark-300 dark:active:bg-dark-200 [&[aria-selected]]:ring-blurple [&[aria-selected]]:ring-width-4 my-1 flex transform-gpu cursor-pointer select-none appearance-none flex-row place-content-center rounded bg-transparent px-4 py-2 text-base font-semibold leading-none text-black outline-0 hover:bg-neutral-100 active:translate-y-px active:bg-neutral-200 dark:text-white [&[aria-selected]]:ring"
key={item.id}
key={`${item.id}-${idx}`}
onSelect={() => {
router.push(item.path);
dialog!.setOpen(false);

View File

@@ -1,7 +1,6 @@
import type { ApiModel, Excerpt } from '@microsoft/api-extractor-model';
import { ExcerptTokenKind } from '@microsoft/api-extractor-model';
import { ItemLink } from './ItemLink';
import { resolveItemURI } from './documentation/util';
export interface ExcerptTextProps {
/**
@@ -29,7 +28,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
}
return (
<ItemLink className="text-blurple" itemURI={resolveItemURI(item)} key={item.containerKey}>
<ItemLink
className="text-blurple"
itemURI={`${item.displayName}:${item.kind}`}
key={`${item.displayName}-${item.containerKey}`}
packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
>
{token.text}
</ItemLink>
);

View File

@@ -1,25 +1,21 @@
'use client';
import { FiCommand } from '@react-icons/all-files/fi/FiCommand';
// import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { VscMenu } from '@react-icons/all-files/vsc/VscMenu';
import { VscSearch } from '@react-icons/all-files/vsc/VscSearch';
import { Button } from 'ariakit/button';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
// import { useTheme } from 'next-themes';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { ThemeSwitcher } from './ThemeSwitcher';
import { useCmdK } from '~/contexts/cmdK';
import { useNav } from '~/contexts/nav';
export function Header() {
const pathname = usePathname();
// eslint-disable-next-line @typescript-eslint/unbound-method
const { setOpened } = useNav();
// const { resolvedTheme, setTheme } = useTheme();
const dialog = useCmdK();
// const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
const [asPathWithoutQueryAndAnchor, setAsPathWithoutQueryAndAnchor] = useState('');
useEffect(() => {
@@ -40,7 +36,7 @@ export function Header() {
<Link
className="focus:ring-width-2 focus:ring-blurple rounded outline-0 hover:underline focus:ring"
href={`/${original.slice(0, idx + 1).join('/')}`}
key={idx}
key={`${path}-${idx}`}
>
{path}
</Link>
@@ -53,7 +49,7 @@ export function Header() {
pathElements.flatMap((el, idx, array) => {
if (idx === 0) {
return (
<Fragment key={idx}>
<Fragment key={`${el.key}-${idx}`}>
<div className="mx-2">/</div>
{el}
<div className="mx-2">/</div>
@@ -63,14 +59,14 @@ export function Header() {
if (idx !== array.length - 1) {
return (
<Fragment key={idx}>
<Fragment key={`${el.key}-${idx}`}>
{el}
<div className="mx-2">/</div>
</Fragment>
);
}
return <Fragment key={idx}>{el}</Fragment>;
return <Fragment key={`${el.key}-${idx}`}>{el}</Fragment>;
}),
[pathElements],
);
@@ -111,13 +107,7 @@ export function Header() {
>
<VscGithubInverted size={24} />
</Button>
{/* <Button
aria-label="Toggle theme"
className="focus:ring-width-2 focus:ring-blurple flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none flex-row place-items-center rounded-full rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-0 focus:ring active:translate-y-px"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button> */}
<ThemeSwitcher />
</div>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { useCurrentPathMeta } from '~/hooks/useCurrentPathMeta';
export interface ItemLinkProps
extends Omit<LinkProps, 'href'>,
@@ -15,6 +16,11 @@ export interface ItemLinkProps
* The URI of the api item to link to. (e.g. `/RestManager`)
*/
itemURI: string;
/**
* The name of the package the item belongs to.
*/
packageName?: string | undefined;
}
/**
@@ -26,17 +32,13 @@ export interface ItemLinkProps
*/
export function ItemLink(props: PropsWithChildren<ItemLinkProps>) {
const path = usePathname();
const { packageName, version } = useCurrentPathMeta();
if (!path) {
throw new Error('ItemLink must be used inside a Next.js page. (e.g. /docs/packages/foo/main)');
}
// Check if the item is already in the current path, if so keep the current path
const end = path?.split('/')?.length < 6 ? path?.length : -1;
const { itemURI, packageName: pkgName, ...linkProps } = props;
const pathPrefix = path?.split('/').slice(0, end).join('/');
const { itemURI, ...linkProps } = props;
return <Link href={`${pathPrefix}${itemURI}`} {...linkProps} />;
return <Link href={`/docs/packages/${pkgName ?? packageName}/${version}/${itemURI}`} {...linkProps} />;
}

View File

@@ -1,3 +0,0 @@
'use client';
export { MDXRemote } from 'next-mdx-remote';

View File

@@ -8,7 +8,6 @@ import { VersionSelect } from './VersionSelect';
import { useNav } from '~/contexts/nav';
export function Nav({ members }: { members: SidebarSectionItemData[] }) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { opened } = useNav();
return (

View File

@@ -24,8 +24,8 @@ export function PackageSelect() {
discord.js
</MenuItem>
</a>,
...PACKAGES.map((pkg) => (
<Link href={`/docs/packages/${pkg}/main`} key={pkg}>
...PACKAGES.map((pkg, idx) => (
<Link href={`/docs/packages/${pkg}/main`} key={`${pkg}-${idx}`}>
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 rounded bg-white p-3 text-sm outline-0 focus:ring"
id={pkg}

View File

@@ -20,9 +20,9 @@ export function PropertyList({ item }: { item: ApiItemContainerMixin }) {
const propertyItems = useMemo(
() =>
members.map((prop) => {
members.map((prop, idx) => {
return (
<Fragment key={prop.item.displayName}>
<Fragment key={`${prop.item.displayName}-${idx}`}>
<Property
inheritedFrom={prop.inherited as ApiDeclaredItem & ApiItemContainerMixin}
item={prop.item as ApiPropertyItem}

View File

@@ -94,7 +94,7 @@ export function Sidebar({ members }: { members: SidebarSectionItemData[] }) {
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
<Section icon={resolveIcon(group)} key={idx} title={group}>
<Section icon={resolveIcon(group)} key={`${group}-${idx}`} title={group}>
{groupItems[group].map((member, index) => (
<ItemLink
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l p-[5px] pl-6 outline-0 focus:rounded focus:border-0 focus:ring ${
@@ -103,7 +103,7 @@ export function Sidebar({ members }: { members: SidebarSectionItemData[] }) {
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'
}`}
itemURI={member.href}
key={index}
key={`${member.name}-${index}`}
onClick={() => setOpened(false)}
title={member.name}
>

View File

@@ -1,32 +1,15 @@
'use client';
import { Code } from 'bright';
import { PrismAsyncLight } from 'react-syntax-highlighter';
import { vscDarkPlus, prism } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export function SyntaxHighlighter({ language = 'typescript', code }: { code: string; language?: string }) {
export function SyntaxHighlighter(props: typeof Code) {
return (
<>
<div data-theme="dark">
<PrismAsyncLight
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
language={language}
style={vscDarkPlus}
wrapLines
wrapLongLines
>
{code}
</PrismAsyncLight>
{/* @ts-expect-error async component */}
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="github-dark-dimmed" />
</div>
<div data-theme="light">
<PrismAsyncLight
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
language={language}
style={prism}
wrapLines
wrapLongLines
>
{code}
</PrismAsyncLight>
{/* @ts-expect-error async component */}
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="min-light" />
</div>
</>
);

View File

@@ -11,10 +11,10 @@ export function Table({
}) {
const cols = useMemo(
() =>
columns.map((column) => (
columns.map((column, idx) => (
<th
className="border-light-900 dark:border-dark-100 break-normal border-b px-3 py-2 text-left text-sm"
key={column}
key={`${column}-${idx}`}
>
{column}
</th>
@@ -26,12 +26,12 @@ export function Table({
() =>
rows.map((row, idx) => (
<tr className="[&>td]:last-of-type:border-0" key={idx}>
{Object.entries(row).map(([colName, val]) => (
{Object.entries(row).map(([colName, val], index) => (
<td
className={`border-light-900 dark:border-dark-100 border-b px-3 py-2 text-left text-sm ${
columnStyles?.[colName] ?? ''
}`}
key={colName}
key={`${colName}-${index}`}
>
{val}
</td>

View File

@@ -27,7 +27,7 @@ export function TableOfContentsPropertyItem({ property }: { property: TableOfCon
<a
className="dark:border-dark-100 border-light-800 dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800 pl-6.5 focus:ring-width-2 focus:ring-blurple ml-[10px] border-l p-[5px] text-sm outline-0 focus:rounded focus:border-0 focus:ring"
href={`#${property.name}`}
key={property.name}
key={`${property.name}-${property.kind}`}
title={property.name}
>
<span className="line-clamp-1">{property.name}</span>
@@ -65,7 +65,7 @@ export function TableOfContentItems({ serializedMembers }: TableOfContentsItemPr
(member): member is TableOfContentsSerializedProperty =>
member.kind === 'Property' || member.kind === 'PropertySignature',
)
.map((prop) => <TableOfContentsPropertyItem key={prop.name} property={prop} />),
.map((prop) => <TableOfContentsPropertyItem key={`${prop.name}-${prop.kind}`} property={prop} />),
[serializedMembers],
);

View File

@@ -0,0 +1,30 @@
'use client';
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import { Button } from 'ariakit/button';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export function ThemeSwitcher() {
const [mounted, setMounted] = useState(false);
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<Button
aria-label="Toggle theme"
className="focus:ring-width-2 focus:ring-blurple flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none flex-row place-items-center rounded-full rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-0 focus:ring active:translate-y-px"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button>
);
}

View File

@@ -18,8 +18,8 @@ export function VersionSelect() {
const versionMenuItems = useMemo(
() =>
versions
?.map((item) => (
<Link href={`/docs/packages/${packageName}/${item}`} key={item}>
?.map((item, idx) => (
<Link href={`/docs/packages/${packageName}/${item}`} key={`${item}-${idx}`}>
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple my-0.5 rounded bg-white p-3 text-sm outline-0 focus:ring"
onClick={() => versionMenu.setOpen(false)}

View File

@@ -42,8 +42,8 @@ export function HierarchyText({ item, type }: { item: ApiClass | ApiInterface; t
<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} />
{excerpts.map((excerpt, idx) => (
<ExcerptText excerpt={excerpt} key={idx} model={model} />
))}
</span>
</div>

View File

@@ -11,15 +11,13 @@ import { hasProperties, hasMethods, serializeMembers } from './util';
export function MemberContainerDocumentation({
item,
version,
subheading,
}: {
item: ApiDeclaredItem & ApiItemContainerMixin & ApiTypeParameterListMixin;
subheading?: ReactNode;
version: string;
}) {
return (
<Documentation item={item}>
<Documentation>
{subheading}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />

View File

@@ -74,7 +74,7 @@ export function TSDoc({ item, tsdoc }: { item: ApiItem; tsdoc: DocNode }): JSX.E
case DocNodeKind.FencedCode: {
const { language, code } = tsdoc as DocFencedCode;
return <SyntaxHighlighter code={code} key={idx} language={language} />;
return <SyntaxHighlighter code={code} key={idx} lang={language ?? 'typescript'} />;
}
case DocNodeKind.Comment: {

View File

@@ -14,8 +14,8 @@ export function Enum({ item }: { item: ApiEnum }) {
<SummarySection item={item} />
<DocumentationSection icon={<VscSymbolEnum size={20} />} padded title="Members">
<div className="flex flex-col gap-4">
{item.members.map((member) => (
<Panel key={member.containerKey}>
{item.members.map((member, idx) => (
<Panel key={`${member.displayName}-${idx}`}>
<EnumMember member={member} />
</Panel>
))}

View File

@@ -9,7 +9,7 @@ export function Function({ item }: { item: ApiFunction }) {
if (item.getMergedSiblings().length > 1) {
const overloads = item
.getMergedSiblings()
.map((sibling, idx) => <FunctionBody item={sibling as ApiFunction} key={idx} />);
.map((sibling, idx) => <FunctionBody item={sibling as ApiFunction} key={`${sibling.displayName}-${idx}`} />);
return (
<div>

View File

@@ -1,7 +1,6 @@
import type { ApiFunction } from '@microsoft/api-extractor-model';
import { SyntaxHighlighter } from '~/components/SyntaxHighlighter';
import { Documentation } from '~/components/documentation/Documentation';
import { Header } from '~/components/documentation/Header';
import { ParameterSection } from '~/components/documentation/section/ParametersSection';
import { SummarySection } from '~/components/documentation/section/SummarySection';
import { TypeParameterSection } from '~/components/documentation/section/TypeParametersSection';
@@ -13,7 +12,7 @@ export interface FunctionBodyProps {
export function FunctionBody({ item }: { item: ApiFunction }) {
return (
<Documentation item={item} showHeader={false}>
<Documentation>
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}

View File

@@ -20,7 +20,9 @@ export function Method({
// each overload node on the server.
const overloads = method
.getMergedSiblings()
.map((sibling, idx) => <MethodDocumentation key={idx} method={sibling as ApiMethod | ApiMethodSignature} />);
.map((sibling, idx) => (
<MethodDocumentation key={`${sibling.displayName}-${idx}`} method={sibling as ApiMethod | ApiMethodSignature} />
));
return (
<OverloadSwitcher overloads={overloads}>

View File

@@ -0,0 +1,19 @@
'use client';
import { usePathname } from 'next/navigation';
export function useCurrentPathMeta() {
const pathname = usePathname();
if (!pathname) {
return {};
}
const [, , , packageName, version, item] = pathname.split('/');
return {
packageName,
version,
item,
};
}

View File

@@ -1,106 +0,0 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/fonts/Inter-Light.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url('/fonts/Inter-LightItalic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/Inter-Regular.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/fonts/Inter-Italic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/Inter-Medium.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('/fonts/Inter-MediumItalic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/Inter-SemiBold.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/fonts/Inter-SemiBoldItalic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/Inter-Bold.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/fonts/Inter-BoldItalic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url('/fonts/Inter-Black.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url('/fonts/Inter-BlackItalic.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url('/fonts/Inter-roman.var.woff2?v=3.19') format('woff2');
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: italic;
font-named-instance: 'Italic';
src: url('/fonts/Inter-italic.var.woff2?v=3.19') format('woff2');
}

View File

@@ -1,30 +1,6 @@
:root {
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
@supports (font-variation-settings: normal) {
:root {
font-family: 'Inter var', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}
}
html,
body {
height: 100%;
color-scheme: light;
}
#__next {
height: 100%;
}
[data-nextjs-scroll-focus-boundary] {
height: 100%;
font-family: var(--font-inter);
min-height: 100vh;
}
[data-theme='dark'] {

View File

@@ -0,0 +1,7 @@
import localFont from 'next/font/local';
export const inter = localFont({
src: '../assets/fonts/Inter.ttf',
variable: '--font-inter',
display: 'swap',
});

View File

@@ -61,7 +61,8 @@
"vitest": "^0.29.7"
},
"resolutions": {
"@microsoft/tsdoc-config": "patch:@microsoft/tsdoc-config@npm%3A0.16.2#./.yarn/patches/@microsoft-tsdoc-config-npm-0.16.2-30fd115d09.patch"
"@microsoft/tsdoc-config@~0.16.1": "patch:@microsoft/tsdoc-config@npm%3A0.16.2#./.yarn/patches/@microsoft-tsdoc-config-npm-0.16.2-30fd115d09.patch",
"@microsoft/tsdoc-config@0.16.2": "patch:@microsoft/tsdoc-config@npm%3A0.16.2#./.yarn/patches/@microsoft-tsdoc-config-npm-0.16.2-30fd115d09.patch"
},
"engines": {
"node": ">=16.9.0"
@@ -70,5 +71,5 @@
"apps/*",
"packages/*"
],
"packageManager": "yarn@3.4.1"
"packageManager": "yarn@3.5.0"
}

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/brokers
yarn add @discordjs/brokers
pnpm add @discordjs/brokers

View File

@@ -20,7 +20,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/builders
yarn add @discordjs/builders
pnpm add @discordjs/builders

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/collection
yarn add @discordjs/collection
pnpm add @discordjs/collection

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/core
yarn add @discordjs/core
pnpm add @discordjs/core

View File

@@ -30,7 +30,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js
@@ -48,7 +48,7 @@ pnpm add discord.js
Install discord.js:
```sh-session
```sh
npm install discord.js
yarn add discord.js
pnpm add discord.js

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/formatters
yarn add @discordjs/formatters
pnpm add @discordjs/formatters

View File

@@ -17,7 +17,7 @@
**Node.js 18.13.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/next
yarn add @discordjs/next
pnpm add @discordjs/next

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/proxy
yarn add @discordjs/proxy
pnpm add @discordjs/proxy

View File

@@ -20,7 +20,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/rest
yarn add @discordjs/rest
pnpm add @discordjs/rest
@@ -30,7 +30,7 @@ pnpm add @discordjs/rest
Install all required dependencies:
```sh-session
```sh
npm install @discordjs/rest discord-api-types
yarn add @discordjs/rest discord-api-types
pnpm add @discordjs/rest discord-api-types

View File

@@ -3,10 +3,10 @@ import { join } from 'node:path';
import { cwd } from 'node:process';
import { generatePath } from '@discordjs/api-extractor-utils';
import {
ApiModel,
ApiDeclaredItem,
ApiItemContainerMixin,
ApiItem,
ApiModel,
type ApiPackage,
ApiItemKind,
} from '@microsoft/api-extractor-model';
@@ -42,8 +42,7 @@ export const PACKAGES = [
];
let idx = 0;
export function createApiModel(data: any) {
const model = new ApiModel();
export function addPackageToModel(model: ApiModel, data: any) {
const tsdocConfiguration = new TSDocConfiguration();
const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig);
tsdocConfigFile.configureParser(tsdocConfiguration);
@@ -163,7 +162,7 @@ export async function generateAllIndices() {
const versionRes = await request(`https://docs.discordjs.dev/docs/${pkg}/${version}.api.json`);
const data = await versionRes.body.json();
const model = createApiModel(data);
const model = addPackageToModel(new ApiModel(), data);
await generateIndex(model, pkg, version);
}
}

View File

@@ -1,5 +1,5 @@
import type { GlobalProvider } from '@ladle/react';
import '@unocss/reset/tailwind.css';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
export const Provider: GlobalProvider = ({ children }) => <>{children}</>;

View File

@@ -17,7 +17,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/util
yarn add @discordjs/util
pnpm add @discordjs/util

View File

@@ -33,7 +33,7 @@ An implementation of the Discord Voice API for Node.js, written in TypeScript.
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/voice
yarn add @discordjs/voice
pnpm add @discordjs/voice

View File

@@ -24,7 +24,7 @@
**Node.js 16.9.0 or newer is required.**
```sh-session
```sh
npm install @discordjs/ws
yarn add @discordjs/ws
pnpm add @discordjs/ws

1121
yarn.lock

File diff suppressed because it is too large Load Diff