From da455bceead521b10c32097b092fccc7a137471f Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:16:54 +0100 Subject: [PATCH] feat: mainlib docs on new website (#9930) * fix(ExceptText): don't display import("d..-types/v10"). in return type * Squashed 'packages/api-extractor-model/' content from commit 39ecb196c git-subtree-dir: packages/api-extractor-model git-subtree-split: 39ecb196ca210bdf84ba6c9cadb1bb93571849d7 * Squashed 'packages/api-extractor/' content from commit 341ad6c51 git-subtree-dir: packages/api-extractor git-subtree-split: 341ad6c51b01656d4f73b74ad4bdb3095f9262c4 * feat(api-extractor): add api-extractor and -model * fix: package.json docs script * fix(SourcLink): use <> instead of function syntax * fix: make packages private * fix: rest params showing in docs, added labels * fix: missed two files * feat: merge docs.json from docgen and docs.api.json * fix: cpy-cli & pnpm-lock * fix: increase icon size * fix: icon size again * feat: run both docs on mainlib * chore: website fixes * fix: more website fixes * fix: tests and dev database script * chore: comment out old docs * fix: increase max fetch cache * fix: env should always be a string * fix: try to reapply patches * fix: remove prepare for docgen * fix: temporary cosmetic fixes * fix: horizontal scroll * feat: generate index for new docs --------- Co-authored-by: Noel --- .github/workflows/documentation.yml | 2 + apps/guide/package.json | 4 +- apps/guide/src/util/constants.ts | 2 +- apps/website/next.config.js | 3 + apps/website/package.json | 4 +- apps/website/scripts/generateAllIndices.js | 12 +- apps/website/src/app/docAPI.ts | 2 +- .../[package]/[version]/[item]/page.tsx | 12 +- .../packages/[package]/[version]/layout.tsx | 9 +- .../src/app/docs/packages/[package]/page.tsx | 16 +- apps/website/src/app/docs/packages/page.tsx | 4 +- apps/website/src/app/page.tsx | 10 +- apps/website/src/components/ExcerptText.tsx | 14 +- apps/website/src/components/PackageSelect.tsx | 25 +- apps/website/src/components/Property.tsx | 2 +- .../src/components/TableOfContentItems.tsx | 43 +- .../src/components/documentation/Members.tsx | 4 +- .../documentation/section/EventsSection.tsx | 42 ++ .../components/documentation/tsdoc/TSDoc.tsx | 4 +- .../src/components/documentation/util.ts | 15 +- apps/website/src/components/model/Event.tsx | 39 ++ .../src/components/model/method/Method.tsx | 33 +- .../model/method/MethodDocumentation.tsx | 2 +- .../components/model/method/MethodHeader.tsx | 2 +- apps/website/src/middleware.ts | 8 +- apps/website/src/util/addPackageToModel.ts | 26 +- apps/website/src/util/constants.ts | 1 + apps/website/src/util/fetchMember.ts | 12 +- apps/website/src/util/model.ts | 4 +- eslint.config.js | 2 +- package.json | 3 +- packages/api-extractor-model/README.md | 4 +- packages/api-extractor-model/src/index.ts | 3 +- .../api-extractor-model/src/items/ApiItem.ts | 8 + .../src/mixins/ApiItemContainerMixin.ts | 102 +++- .../api-extractor-model/src/mixins/Excerpt.ts | 6 +- .../api-extractor-model/src/model/ApiClass.ts | 31 +- .../api-extractor-model/src/model/ApiEvent.ts | 72 +++ .../src/model/ApiInterface.ts | 13 +- .../src/model/Deserializer.ts | 486 +++++++++++++++- .../src/model/HeritageType.ts | 5 +- packages/api-extractor/README.md | 4 +- .../api-extractor/extends/tsdoc-base.json | 2 +- .../src/analyzer/PackageMetadataManager.ts | 2 +- .../test/PackageMetadataManager.test.ts | 2 - .../api-extractor/src/api/ExtractorConfig.ts | 2 +- .../src/generators/ApiModelGenerator.ts | 538 ++++++++++++++++-- .../src/generators/ExcerptBuilder.ts | 11 +- packages/api-extractor/src/index.ts | 2 +- packages/discord.js/api-extractor.json | 7 + packages/discord.js/package.json | 4 +- .../discord.js/typings/tsdoc-metadata.json | 11 + packages/docgen/bin/index.js | 2 + packages/docgen/bin/index.ts | 1 + packages/docgen/package.json | 3 +- packages/docgen/src/documentation.ts | 180 ++++++ packages/docgen/src/index.ts | 7 +- packages/scripts/package.json | 7 +- packages/scripts/src/generateIndex.ts | 52 +- .../scripts/src/populateDevDatabaseBranch.ts | 32 ++ packages/scripts/tsup.config.ts | 13 +- patches/next@14.0.2-canary.20.patch | 36 ++ pnpm-lock.yaml | 123 ++-- 63 files changed, 1878 insertions(+), 254 deletions(-) create mode 100644 apps/website/src/components/documentation/section/EventsSection.tsx create mode 100644 apps/website/src/components/model/Event.tsx create mode 100644 packages/api-extractor-model/src/model/ApiEvent.ts create mode 100644 packages/discord.js/api-extractor.json create mode 100644 packages/discord.js/typings/tsdoc-metadata.json create mode 100755 packages/docgen/bin/index.js create mode 100644 packages/scripts/src/populateDevDatabaseBranch.ts create mode 100644 patches/next@14.0.2-canary.20.patch diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e9a9fa32d9c8..816a7b2e42a7 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -88,6 +88,7 @@ jobs: mkdir -p "out/${PACKAGE}" if [[ "${PACKAGE}" == "discord.js" ]]; then mv "packages/${PACKAGE}/docs/docs.json" "out/${PACKAGE}/${SEMVER}.json" + mv "packages/${PACKAGE}/docs/docs.api.json" "out/${PACKAGE}/${SEMVER}.api.json" else mv "packages/${PACKAGE}/docs/docs.api.json" "out/${PACKAGE}/${SEMVER}.api.json" fi @@ -106,6 +107,7 @@ jobs: if [[ "${PACKAGE}" == "discord.js" ]]; then mkdir -p "out/${PACKAGE}" mv "packages/${PACKAGE}/docs/docs.json" "out/${PACKAGE}/${GITHUB_REF_NAME}.json" + mv "packages/${PACKAGE}/docs/docs.api.json" "out/${PACKAGE}/${GITHUB_REF_NAME}.api.json" else mkdir -p "out/${PACKAGE}" mv "packages/${PACKAGE}/docs/docs.api.json" "out/${PACKAGE}/${GITHUB_REF_NAME}.api.json" diff --git a/apps/guide/package.json b/apps/guide/package.json index b6fdf545052e..02770fcc2bd1 100644 --- a/apps/guide/package.json +++ b/apps/guide/package.json @@ -54,7 +54,7 @@ "ariakit": "2.0.0-next.44", "cmdk": "^0.2.0", "contentlayer": "^0.3.4", - "next": "^14.0.2-canary.14", + "next": "14.0.2-canary.20", "next-contentlayer": "^0.3.4", "next-themes": "^0.2.1", "react": "^18.2.0", @@ -66,7 +66,7 @@ "sharp": "^0.32.6" }, "devDependencies": { - "@next/bundle-analyzer": "^14.0.1", + "@next/bundle-analyzer": "14.0.2-canary.20", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/html-escaper": "^3.0.1", diff --git a/apps/guide/src/util/constants.ts b/apps/guide/src/util/constants.ts index d2ee15d21901..ee870b2b9cc7 100644 --- a/apps/guide/src/util/constants.ts +++ b/apps/guide/src/util/constants.ts @@ -26,7 +26,7 @@ export const PACKAGES = [ /** * The stable version of discord.js. */ -export const VERSION = '14.11.0' as const; +export const VERSION = '14.13.0' as const; /** * The API version (for discord-api-types). This is prefixed with a "v". diff --git a/apps/website/next.config.js b/apps/website/next.config.js index 1b304137913e..a6fe69f2198f 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.js @@ -16,6 +16,9 @@ export default withBundleAnalyzer({ contentSecurityPolicy: "default-src 'self'; frame-src 'none'; sandbox;", }, poweredByHeader: false, + env: { + MAX_FETCH_SIZE: '5', + }, async redirects() { return [ { diff --git a/apps/website/package.json b/apps/website/package.json index a095b29d55a1..93cfe0c92965 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -63,7 +63,7 @@ "class-variance-authority": "^0.7.0", "cmdk": "^0.2.0", "meilisearch": "^0.35.0", - "next": "^14.0.2-canary.14", + "next": "14.0.2-canary.20", "next-mdx-remote": "^4.4.1", "next-themes": "^0.2.1", "react": "^18.2.0", @@ -75,7 +75,7 @@ "sharp": "^0.32.6" }, "devDependencies": { - "@next/bundle-analyzer": "^14.0.1", + "@next/bundle-analyzer": "14.0.2-canary.20", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/node": "18.18.8", diff --git a/apps/website/scripts/generateAllIndices.js b/apps/website/scripts/generateAllIndices.js index dda074cdc51d..b60115cb57ec 100644 --- a/apps/website/scripts/generateAllIndices.js +++ b/apps/website/scripts/generateAllIndices.js @@ -1,5 +1,15 @@ +import { readFile } from 'node:fs/promises'; import { generateAllIndices } from '@discordjs/scripts'; console.log('Generating all indices...'); -await generateAllIndices(); +await generateAllIndices({ + fetchPackageVersions: async (pkg) => { + console.log(`Fetching versions for ${pkg}...`); + return ['main']; + }, + fetchPackageVersionDocs: async (pkg, version) => { + console.log(`Fetching data for ${pkg} ${version}...`); + return JSON.parse(await readFile(`${process.cwd()}/../../packages/${pkg}/docs/docs.api.json`, 'utf8')); + }, +}); console.log('Generated all indices.'); diff --git a/apps/website/src/app/docAPI.ts b/apps/website/src/app/docAPI.ts index 5a846abdef5e..f6fea6e894ba 100644 --- a/apps/website/src/app/docAPI.ts +++ b/apps/website/src/app/docAPI.ts @@ -23,7 +23,7 @@ export async function fetchVersions(packageName: string): Promise { return rows.map((row) => row.version); } -export async function fetchModelJSON(packageName: string, version: string): Promise { +export async function fetchModelJSON(packageName: string, version: string): Promise { if (process.env.NEXT_PUBLIC_LOCAL_DEV) { const res = await readFile( join(process.cwd(), '..', '..', 'packages', packageName, 'docs', 'docs.api.json'), diff --git a/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx b/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx index 7a12facb169d..097906880b76 100644 --- a/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx +++ b/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx @@ -32,9 +32,15 @@ import { findMember } from '~/util/model'; async function fetchHeadMember({ package: packageName, version, item }: ItemRouteParams) { const modelJSON = await fetchModelJSON(packageName, version); + + if (!modelJSON) { + return undefined; + } + const model = addPackageToModel(new ApiModel(), modelJSON); const pkg = model.tryGetPackageByName(packageName); const entry = pkg?.entryPoints[0]; + if (!entry) { return undefined; } @@ -88,7 +94,11 @@ export async function generateMetadata({ params }: { params: ItemRouteParams }) const searchParams = resolveMemberSearchParams(params.package, member); url.search = searchParams.toString(); const ogImage = url.toString(); - const description = tryResolveSummaryText(member as ApiDeclaredItem); + let description; + + if (member) { + description = tryResolveSummaryText(member as ApiDeclaredItem); + } return { title: name, diff --git a/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx b/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx index cfa8a13ab431..a6b440e44d8d 100644 --- a/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx +++ b/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx @@ -4,7 +4,7 @@ import dynamic from 'next/dynamic'; import { notFound } from 'next/navigation'; import type { PropsWithChildren } from 'react'; import { fetchModelJSON, fetchVersions } from '~/app/docAPI'; -import { Banner } from '~/components/Banner'; +// import { Banner } from '~/components/Banner'; import { CmdKDialog } from '~/components/CmdK'; import { Nav } from '~/components/Nav'; import type { SidebarSectionItemData } from '~/components/Sidebar'; @@ -46,6 +46,11 @@ function serializeIntoSidebarItemData(item: ApiItem): SidebarSectionItemData { export default async function PackageLayout({ children, params }: PropsWithChildren<{ params: VersionRouteParams }>) { const modelJSON = await fetchModelJSON(params.package, params.version); + + if (!modelJSON) { + notFound(); + } + const model = addPackageToModel(new ApiModel(), modelJSON); const pkg = model.tryGetPackageByName(params.package); @@ -72,7 +77,7 @@ export default async function PackageLayout({ children, params }: PropsWithChild return ( - + {/* */}
diff --git a/apps/website/src/app/docs/packages/[package]/page.tsx b/apps/website/src/app/docs/packages/[package]/page.tsx index 7f12db8fdc6d..fda12935605d 100644 --- a/apps/website/src/app/docs/packages/[package]/page.tsx +++ b/apps/website/src/app/docs/packages/[package]/page.tsx @@ -7,22 +7,12 @@ import { fetchVersions } from '~/app/docAPI'; import { buttonVariants } from '~/styles/Button'; import { PACKAGES } from '~/util/constants'; -async function getData(pkg: string) { - if (!PACKAGES.includes(pkg)) { +export default async function Page({ params }: { params: { package: string } }) { + if (!PACKAGES.includes(params.package)) { notFound(); } - const data = await fetchVersions(pkg); - - if (!data.length) { - throw new Error('Failed to fetch data'); - } - - return data; -} - -export default async function Page({ params }: { params: { package: string } }) { - const data = await getData(params.package); + const data = await fetchVersions(params.package); return (
diff --git a/apps/website/src/app/docs/packages/page.tsx b/apps/website/src/app/docs/packages/page.tsx index 38dbba03e509..8b22ef47d424 100644 --- a/apps/website/src/app/docs/packages/page.tsx +++ b/apps/website/src/app/docs/packages/page.tsx @@ -11,7 +11,7 @@ export default function Page() {

Select a package:

- + {/*
@@ -19,7 +19,7 @@ export default function Page() {
-
+ */} {PACKAGES.map((pkg, idx) => ( - + {/* */}
@@ -22,11 +22,11 @@ export default function Page() {

{DESCRIPTION}

- + {/* Docs - + */} - Module docs + Docs {excerpt.spannedTokens.map((token, idx) => { + // TODO: Real fix in api-extractor needed + const text = token.text.replaceAll('\n', '').replaceAll(/\s{2}$/g, ''); if (token.kind === ExcerptTokenKind.Reference) { const source = token.canonicalReference?.source; const symbol = token.canonicalReference?.symbol; @@ -32,12 +34,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) { // dapi-types doesn't have routes for class members // so we can assume this member is for an enum if (meaning === 'member' && path && 'parent' in path) href += `/enum/${path.parent}#${path.component}`; - else if (meaning === 'type') href += `#${token.text}`; - else href += `/${meaning}/${token.text}`; + else if (meaning === 'type') href += `#${text}`; + else href += `/${meaning}/${text}`; return ( - {token.text} + {text} ); } @@ -45,7 +47,7 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) { const item = model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem; if (!item) { - return token.text; + return text; } return ( @@ -55,12 +57,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) { key={`${item.displayName}-${item.containerKey}-${idx}`} packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')} > - {token.text} + {text} ); } - return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, ''); + return text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, ''); })} ); diff --git a/apps/website/src/components/PackageSelect.tsx b/apps/website/src/components/PackageSelect.tsx index d9987af1829a..c6b8c2d7224c 100644 --- a/apps/website/src/components/PackageSelect.tsx +++ b/apps/website/src/components/PackageSelect.tsx @@ -15,18 +15,18 @@ export default function PackageSelect() { const packageMenu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true }); const packageMenuItems = useMemo( - () => [ - - packageMenu.setOpen(false)} - state={packageMenu} - > - discord.js - - , - ...PACKAGES.map((pkg, idx) => ( + () => + // + // packageMenu.setOpen(false)} + // state={packageMenu} + // > + // discord.js + // + // , + PACKAGES.map((pkg, idx) => ( )), - ], [packageMenu], ); diff --git a/apps/website/src/components/Property.tsx b/apps/website/src/components/Property.tsx index 75739f3de812..5a30d8860fa8 100644 --- a/apps/website/src/components/Property.tsx +++ b/apps/website/src/components/Property.tsx @@ -38,7 +38,7 @@ export function Property({
{hasSummary || inheritedFrom ? ( -
+
{item.tsdocComment ? : null} {inheritedFrom ? : null} {children} diff --git a/apps/website/src/components/TableOfContentItems.tsx b/apps/website/src/components/TableOfContentItems.tsx index 5aba8edc106f..544b25c785d9 100644 --- a/apps/website/src/components/TableOfContentItems.tsx +++ b/apps/website/src/components/TableOfContentItems.tsx @@ -1,6 +1,7 @@ 'use client'; import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection'; +import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty'; import { useMemo } from 'react'; @@ -16,7 +17,15 @@ export interface TableOfContentsSerializedProperty { name: string; } -export type TableOfContentsSerialized = TableOfContentsSerializedMethod | TableOfContentsSerializedProperty; +export interface TableOfContentsSerializedEvent { + kind: 'Event'; + name: string; +} + +export type TableOfContentsSerialized = + | TableOfContentsSerializedEvent + | TableOfContentsSerializedMethod + | TableOfContentsSerializedProperty; export interface TableOfContentsItemProps { readonly serializedMembers: TableOfContentsSerialized[]; @@ -57,6 +66,19 @@ export function TableOfContentsMethodItem({ method }: { readonly method: TableOf ); } +export function TableOfContentsEventItem({ event }: { readonly event: TableOfContentsSerializedEvent }) { + return ( + + {event.name} + + ); +} + export function TableOfContentItems({ serializedMembers }: TableOfContentsItemProps) { const propertyItems = useMemo( () => @@ -85,6 +107,14 @@ export function TableOfContentItems({ serializedMembers }: TableOfContentsItemPr [serializedMembers], ); + const eventItems = useMemo( + () => + serializedMembers + .filter((member): member is TableOfContentsSerializedEvent => member.kind === 'Event') + .map((event, idx) => ), + [serializedMembers], + ); + return (
@@ -92,6 +122,17 @@ export function TableOfContentItems({ serializedMembers }: TableOfContentsItemPr Contents
+ {eventItems.length ? ( +
+
+ +
+ Properties +
+
+ {eventItems} +
+ ) : null} {propertyItems.length ? (
diff --git a/apps/website/src/components/documentation/Members.tsx b/apps/website/src/components/documentation/Members.tsx index 42f9ef01a595..64a69a4028c6 100644 --- a/apps/website/src/components/documentation/Members.tsx +++ b/apps/website/src/components/documentation/Members.tsx @@ -1,11 +1,13 @@ import type { ApiDeclaredItem, ApiItemContainerMixin } from '@discordjs/api-extractor-model'; +import { EventsSection } from './section/EventsSection'; import { MethodsSection } from './section/MethodsSection'; import { PropertiesSection } from './section/PropertiesSection'; -import { hasProperties, hasMethods } from './util'; +import { hasEvents, hasProperties, hasMethods } from './util'; export function Members({ item }: { readonly item: ApiDeclaredItem & ApiItemContainerMixin }) { return ( <> + {hasEvents(item) ? : null} {hasProperties(item) ? : null} {hasMethods(item) ? : null} diff --git a/apps/website/src/components/documentation/section/EventsSection.tsx b/apps/website/src/components/documentation/section/EventsSection.tsx new file mode 100644 index 000000000000..a91164c4eb21 --- /dev/null +++ b/apps/website/src/components/documentation/section/EventsSection.tsx @@ -0,0 +1,42 @@ +import { + ApiItemKind, + type ApiEvent, + type ApiItem, + type ApiItemContainerMixin, + type ApiDeclaredItem, +} from '@discordjs/api-extractor-model'; +import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent'; +import { Fragment, useMemo } from 'react'; +import { Event } from '~/components/model/Event'; +import { resolveMembers } from '~/util/members'; +import { DocumentationSection } from './DocumentationSection'; + +function isEventLike(item: ApiItem): item is ApiEvent { + return item.kind === ApiItemKind.Event; +} + +export function EventsSection({ item }: { readonly item: ApiItemContainerMixin }) { + const members = resolveMembers(item, isEventLike); + + const eventItems = useMemo( + () => + members.map((event, idx) => { + return ( + + +
+ + ); + }), + [members], + ); + + return ( + } padded title="Events"> +
{eventItems}
+
+ ); +} diff --git a/apps/website/src/components/documentation/tsdoc/TSDoc.tsx b/apps/website/src/components/documentation/tsdoc/TSDoc.tsx index 37d4eb4414a4..3572ad128ce7 100644 --- a/apps/website/src/components/documentation/tsdoc/TSDoc.tsx +++ b/apps/website/src/components/documentation/tsdoc/TSDoc.tsx @@ -32,7 +32,9 @@ export function TSDoc({ item, tsdoc }: { readonly item: ApiItem; readonly tsdoc: const { codeDestination, urlDestination, linkText } = tsdoc as DocLinkTag; if (codeDestination) { - const foundItem = item.getAssociatedModel()?.resolveDeclarationReference(codeDestination, item) + // TODO: Real fix in api-extractor needed + const currentItem = item.getAssociatedPackage(); + const foundItem = item.getAssociatedModel()?.resolveDeclarationReference(codeDestination, currentItem) .resolvedApiItem; if (!foundItem) return null; diff --git a/apps/website/src/components/documentation/util.ts b/apps/website/src/components/documentation/util.ts index aca3c5260479..f7acd503ee34 100644 --- a/apps/website/src/components/documentation/util.ts +++ b/apps/website/src/components/documentation/util.ts @@ -8,6 +8,7 @@ import type { ApiPropertySignature, ApiDocumentedItem, ApiParameterListMixin, + ApiEvent, } from '@discordjs/api-extractor-model'; import { METHOD_SEPARATOR, OVERLOAD_SEPARATOR } from '~/util/constants'; import { resolveMembers } from '~/util/members'; @@ -26,6 +27,10 @@ export function hasMethods(item: ApiItemContainerMixin) { ); } +export function hasEvents(item: ApiItemContainerMixin) { + return resolveMembers(item, memberPredicate).some(({ item: member }) => member.kind === ApiItemKind.Event); +} + export function resolveItemURI(item: ApiItem): string { return !item.parent || item.parent.kind === ApiItemKind.EntryPoint ? `${item.displayName}${OVERLOAD_SEPARATOR}${item.kind}` @@ -34,12 +39,13 @@ export function resolveItemURI(item: ApiItem): string { export function memberPredicate( item: ApiItem, -): item is ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature { +): item is ApiEvent | ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature { return ( item.kind === ApiItemKind.Property || item.kind === ApiItemKind.PropertySignature || item.kind === ApiItemKind.Method || - item.kind === ApiItemKind.MethodSignature + item.kind === ApiItemKind.MethodSignature || + item.kind === ApiItemKind.Event ); } @@ -50,6 +56,11 @@ export function serializeMembers(clazz: ApiItemContainerMixin): TableOfContentsS kind: member.kind as 'Method' | 'MethodSignature', name: member.displayName, }; + } else if (member.kind === 'Event') { + return { + kind: member.kind as 'Event', + name: member.displayName, + }; } else { return { kind: member.kind as 'Property' | 'PropertySignature', diff --git a/apps/website/src/components/model/Event.tsx b/apps/website/src/components/model/Event.tsx new file mode 100644 index 000000000000..27d8b1e23c7e --- /dev/null +++ b/apps/website/src/components/model/Event.tsx @@ -0,0 +1,39 @@ +import type { ApiDeclaredItem, ApiItemContainerMixin, ApiEvent } from '@discordjs/api-extractor-model'; +import { Badges } from '../Badges'; +import { CodeHeading } from '../CodeHeading'; +import { InheritanceText } from '../InheritanceText'; +import { ParameterTable } from '../ParameterTable'; +import { TSDoc } from '../documentation/tsdoc/TSDoc'; +import { parametersString } from '../documentation/util'; + +export function Event({ + item, + inheritedFrom, +}: { + readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined; + readonly item: ApiEvent; +}) { + const hasSummary = Boolean(item.tsdocComment?.summarySection); + + return ( +
+
+ + + {`${item.name}(${parametersString(item)})`} + +
+ {hasSummary || inheritedFrom ? ( +
+ {item.tsdocComment ? : null} + {item.parameters.length ? : null} + {inheritedFrom ? : null} +
+ ) : null} +
+ ); +} diff --git a/apps/website/src/components/model/method/Method.tsx b/apps/website/src/components/model/method/Method.tsx index d18fc620671d..c42158403b09 100644 --- a/apps/website/src/components/model/method/Method.tsx +++ b/apps/website/src/components/model/method/Method.tsx @@ -1,8 +1,9 @@ -import type { - ApiDeclaredItem, - ApiItemContainerMixin, - ApiMethod, - ApiMethodSignature, +import { + ApiItemKind, + type ApiDeclaredItem, + type ApiItemContainerMixin, + type ApiMethod, + type ApiMethodSignature, } from '@discordjs/api-extractor-model'; import dynamic from 'next/dynamic'; import { Fragment } from 'react'; @@ -18,15 +19,23 @@ export function Method({ readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined; readonly method: ApiMethod | ApiMethodSignature; }) { - if (method.getMergedSiblings().length > 1) { + if ( + method + .getMergedSiblings() + .filter((sibling) => sibling.kind === ApiItemKind.Method || sibling.kind === ApiItemKind.MethodSignature).length > + 1 + ) { // We have overloads, use the overload switcher, but render // each overload node on the server. - const overloads = method.getMergedSiblings().map((sibling, idx) => ( - - - - - )); + const overloads = method + .getMergedSiblings() + .filter((sibling) => sibling.kind === ApiItemKind.Method || sibling.kind === ApiItemKind.MethodSignature) + .map((sibling, idx) => ( + + + + + )); return ; } diff --git a/apps/website/src/components/model/method/MethodDocumentation.tsx b/apps/website/src/components/model/method/MethodDocumentation.tsx index 889f0c8d3f8f..27497dc06788 100644 --- a/apps/website/src/components/model/method/MethodDocumentation.tsx +++ b/apps/website/src/components/model/method/MethodDocumentation.tsx @@ -21,7 +21,7 @@ export function MethodDocumentation({ method, inheritedFrom }: MethodDocumentati } return ( -
+
{method.tsdocComment ? : null} {method.parameters.length ? : null} {inheritedFrom && parent ? : null} diff --git a/apps/website/src/components/model/method/MethodHeader.tsx b/apps/website/src/components/model/method/MethodHeader.tsx index 8b03984d2c16..1edb2ccc4d16 100644 --- a/apps/website/src/components/model/method/MethodHeader.tsx +++ b/apps/website/src/components/model/method/MethodHeader.tsx @@ -12,7 +12,7 @@ export function MethodHeader({ method }: { readonly method: ApiMethod | ApiMetho ); return ( -
+
row.version).at(1); } export default async function middleware(request: NextRequest) { @@ -35,9 +35,9 @@ export default async function middleware(request: NextRequest) { } catch {} } - if (request.nextUrl.pathname.includes('discord.js')) { + /* if (request.nextUrl.pathname.includes('discord.js')) { return NextResponse.redirect('https://old.discordjs.dev/#/docs/discord.js'); - } + } */ if (PACKAGES.some((pkg) => request.nextUrl.pathname.includes(pkg))) { // eslint-disable-next-line prefer-named-capture-group @@ -52,5 +52,5 @@ export default async function middleware(request: NextRequest) { } export const config = { - matcher: ['/docs', '/docs/packages/discord.js(.*)?', '/docs/packages/:package/stable/:member*'], + matcher: ['/docs', /* '/docs/packages/discord.js(.*)?',*/ '/docs/packages/:package/stable/:member*'], }; diff --git a/apps/website/src/util/addPackageToModel.ts b/apps/website/src/util/addPackageToModel.ts index 6dd4f069cc15..42d0d3f4e277 100644 --- a/apps/website/src/util/addPackageToModel.ts +++ b/apps/website/src/util/addPackageToModel.ts @@ -4,17 +4,23 @@ import { TSDocConfiguration } from '@microsoft/tsdoc'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; export function addPackageToModel(model: ApiModel, data: any) { - const tsdocConfiguration = new TSDocConfiguration(); - const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig); - tsdocConfigFile.configureParser(tsdocConfiguration); + let apiPackage: ApiPackage; + if (data.metadata) { + const tsdocConfiguration = new TSDocConfiguration(); + const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig); + tsdocConfigFile.configureParser(tsdocConfiguration); + + apiPackage = ApiItem.deserialize(data, { + apiJsonFilename: '', + toolPackage: data.metadata.toolPackage, + toolVersion: data.metadata.toolVersion, + versionToDeserialize: data.metadata.schemaVersion, + tsdocConfiguration, + }) as ApiPackage; + } else { + apiPackage = ApiItem.deserializeDocgen(data, 'discord.js') as ApiPackage; + } - const apiPackage = ApiItem.deserialize(data, { - apiJsonFilename: '', - toolPackage: data.metadata.toolPackage, - toolVersion: data.metadata.toolVersion, - versionToDeserialize: data.metadata.schemaVersion, - tsdocConfiguration, - }) as ApiPackage; model.addMember(apiPackage); return model; } diff --git a/apps/website/src/util/constants.ts b/apps/website/src/util/constants.ts index cedd0d7ec632..aa5f86e296d6 100644 --- a/apps/website/src/util/constants.ts +++ b/apps/website/src/util/constants.ts @@ -1,4 +1,5 @@ export const PACKAGES = [ + 'discord.js', 'brokers', 'builders', 'collection', diff --git a/apps/website/src/util/fetchMember.ts b/apps/website/src/util/fetchMember.ts index 535aa49fbefe..a72e8c0a0e93 100644 --- a/apps/website/src/util/fetchMember.ts +++ b/apps/website/src/util/fetchMember.ts @@ -1,5 +1,4 @@ import { ApiModel, ApiFunction } from '@discordjs/api-extractor-model'; -import { notFound } from 'next/navigation'; import { fetchModelJSON } from '~/app/docAPI'; import { addPackageToModel } from './addPackageToModel'; import { OVERLOAD_SEPARATOR, PACKAGES } from './constants'; @@ -13,7 +12,7 @@ export interface ItemRouteParams { export async function fetchMember({ package: packageName, version: branchName = 'main', item }: ItemRouteParams) { if (!PACKAGES.includes(packageName)) { - notFound(); + return null; } const model = new ApiModel(); @@ -22,10 +21,19 @@ export async function fetchMember({ package: packageName, version: branchName = const modelJSONFiles = await Promise.all(PACKAGES.map(async (pkg) => fetchModelJSON(pkg, branchName))); for (const modelJSONFile of modelJSONFiles) { + if (!modelJSONFile) { + continue; + } + addPackageToModel(model, modelJSONFile); } } else { const modelJSON = await fetchModelJSON(packageName, branchName); + + if (!modelJSON) { + return null; + } + addPackageToModel(model, modelJSON); } diff --git a/apps/website/src/util/model.ts b/apps/website/src/util/model.ts index 75d10f482668..ea2bf194d269 100644 --- a/apps/website/src/util/model.ts +++ b/apps/website/src/util/model.ts @@ -8,7 +8,7 @@ import type { import type { DocSection } from '@microsoft/tsdoc'; export function findMemberByKey(model: ApiModel, packageName: string, containerKey: string) { - const pkg = model.tryGetPackageByName(`@discordjs/${packageName}`)!; + const pkg = model.tryGetPackageByName(packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`)!; return (pkg.members[0] as ApiEntryPoint).tryGetMemberByKey(containerKey); } @@ -17,7 +17,7 @@ export function findMember(model: ApiModel, packageName: string, memberName: str return undefined; } - const pkg = model.tryGetPackageByName(`@discordjs/${packageName}`)!; + const pkg = model.tryGetPackageByName(packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`)!; return pkg.entryPoints[0]?.findMembersByName(memberName)[0]; } diff --git a/eslint.config.js b/eslint.config.js index 26c8a9756688..024dbe7b8fbf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -75,7 +75,7 @@ export default [ rules: { 'jsdoc/no-undefined-types': 0 }, }, { - files: [`packages/{brokers,create-discord-bot,docgen,ws}/**/*${commonFiles}`], + files: [`packages/{api-extractor,brokers,create-discord-bot,docgen,ws}/**/*${commonFiles}`], rules: { 'n/no-sync': 0 }, }, { diff --git a/package.json b/package.json index faaa5fd36a87..21307a2aa38e 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "@contentlayer/utils>@opentelemetry/semantic-conventions": "^1.15.1" }, "patchedDependencies": { - "@microsoft/tsdoc-config@0.16.2": "patches/@microsoft__tsdoc-config@0.16.2.patch" + "@microsoft/tsdoc-config@0.16.2": "patches/@microsoft__tsdoc-config@0.16.2.patch", + "next@14.0.2-canary.20": "patches/next@14.0.2-canary.20.patch" } }, "engines": { diff --git a/packages/api-extractor-model/README.md b/packages/api-extractor-model/README.md index 8c5c794dc488..ded4e361bb4f 100644 --- a/packages/api-extractor-model/README.md +++ b/packages/api-extractor-model/README.md @@ -1,4 +1,4 @@ -# @microsoft/api-extractor-model +# @discordjs/api-extractor-model Use this library to read and write \*.api.json files as defined by the [API Extractor](https://api-extractor.com/) tool. These files are used to generate a documentation website for your TypeScript package. The files store the @@ -60,7 +60,7 @@ a namespace containing static members of the class. ## Links -- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/libraries/api-extractor-model/CHANGELOG.md) - Find +- [CHANGELOG.md](https://github.com/discordjs/discord.js/blob/main/packages/api-extractor-model/CHANGELOG.md) - Find out what's new in the latest version - [API Reference](https://rushstack.io/pages/api/api-extractor-model/) diff --git a/packages/api-extractor-model/src/index.ts b/packages/api-extractor-model/src/index.ts index 46905b8b0cf4..cc7b31fdb60a 100644 --- a/packages/api-extractor-model/src/index.ts +++ b/packages/api-extractor-model/src/index.ts @@ -58,12 +58,13 @@ export type { Constructor, PropertiesOf } from './mixins/Mixin.js'; // model export { type IApiCallSignatureOptions, ApiCallSignature } from './model/ApiCallSignature.js'; -export { type IApiClassOptions, ApiClass } from './model/ApiClass.js'; +export { type IApiClassOptions, ApiClass, type IExcerptTokenRangeWithTypeParameters } from './model/ApiClass.js'; export { type IApiConstructorOptions, ApiConstructor } from './model/ApiConstructor.js'; export { type IApiConstructSignatureOptions, ApiConstructSignature } from './model/ApiConstructSignature.js'; export { type IApiEntryPointOptions, ApiEntryPoint } from './model/ApiEntryPoint.js'; export { type IApiEnumOptions, ApiEnum } from './model/ApiEnum.js'; export { type IApiEnumMemberOptions, ApiEnumMember, EnumMemberOrder } from './model/ApiEnumMember.js'; +export { type IApiEventOptions, ApiEvent } from './model/ApiEvent.js'; export { type IApiFunctionOptions, ApiFunction } from './model/ApiFunction.js'; export { type IApiIndexSignatureOptions, ApiIndexSignature } from './model/ApiIndexSignature.js'; export { type IApiInterfaceOptions, ApiInterface } from './model/ApiInterface.js'; diff --git a/packages/api-extractor-model/src/items/ApiItem.ts b/packages/api-extractor-model/src/items/ApiItem.ts index 5cabaae124c2..e6e3fcaecb1c 100644 --- a/packages/api-extractor-model/src/items/ApiItem.ts +++ b/packages/api-extractor-model/src/items/ApiItem.ts @@ -8,6 +8,7 @@ import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; import type { Constructor, PropertiesOf } from '../mixins/Mixin.js'; import type { ApiModel } from '../model/ApiModel.js'; import type { ApiPackage } from '../model/ApiPackage.js'; +import type { DocgenJson } from '../model/Deserializer'; import type { DeserializerContext } from '../model/DeserializerContext.js'; /** @@ -24,6 +25,7 @@ export enum ApiItemKind { EntryPoint = 'EntryPoint', Enum = 'Enum', EnumMember = 'EnumMember', + Event = 'Event', Function = 'Function', IndexSignature = 'IndexSignature', Interface = 'Interface', @@ -111,6 +113,12 @@ export class ApiItem { return deserializerModule.Deserializer.deserialize(context, jsonObject); } + public static deserializeDocgen(jsonObject: DocgenJson, _package: string): ApiItem { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const deserializerModule: typeof import('../model/Deserializer') = require('../model/Deserializer'); + return deserializerModule.Deserializer.deserializeDocgen(jsonObject, _package); + } + /** * @virtual */ diff --git a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts index 5959e03cfc28..1788dfed461b 100644 --- a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts +++ b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts @@ -3,6 +3,7 @@ import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; import { InternalError } from '@rushstack/node-core-library'; +import type { ApiDeclaredItem } from '../index.js'; import { ApiItem, apiItem_onParentChanged, @@ -36,6 +37,11 @@ export interface IApiItemContainerJson extends IApiItemJson { preserveMemberOrder?: boolean; } +interface IMappedTypeParameters { + item: ApiItem; + mappedTypeParameters: Map; +} + const _members: unique symbol = Symbol('ApiItemContainerMixin._members'); const _membersSorted: unique symbol = Symbol('ApiItemContainerMixin._membersSorted'); const _membersByContainerKey: unique symbol = Symbol('ApiItemContainerMixin._membersByContainerKey'); @@ -299,23 +305,76 @@ export function ApiItemContainerMixin( } } + // The Deserializer class is coupled with a ton of other classes, so we delay loading it + // to avoid ES5 circular imports. + // *eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + // const deserializerModule: typeof import('../model/Deserializer') = require('../model/Deserializer'); + const membersByName: Map = new Map(); const membersByKind: Map = new Map(); - const toVisit: ApiItem[] = []; - let next: ApiItem | undefined = this; + const toVisit: IMappedTypeParameters[] = []; + let next: IMappedTypeParameters | undefined = { item: this, mappedTypeParameters: new Map() }; - while (next) { - const membersToAdd: ApiItem[] = []; + while (next?.item) { + const membersToAdd: ApiItem[] = []; /* + const typeParams = next.mappedTypeParameters; + const context: DeserializerContext = { + apiJsonFilename: '', + toolPackage: '', + toolVersion: '', + versionToDeserialize: ApiJsonSchemaVersion.LATEST, + tsdocConfiguration: new TSDocConfiguration(), + }; */ // For each member, check to see if we've already seen a member with the same name // previously in the inheritance tree. If so, we know we won't inherit it, and thus // do not add it to our `membersToAdd` array. - for (const member of next.members) { + for (const member of next.item.members) { // We add the to-be-added members to an intermediate array instead of immediately // to the maps themselves to support method overloads with the same name. if (ApiNameMixin.isBaseClassOf(member)) { if (!membersByName.has(member.name)) { + // This was supposed to replace type parameters with their assigned values in inheritance, but doesn't work yet + /* + if ( + ApiTypeParameterListMixin.isBaseClassOf(member) && + member.typeParameters.some((param) => typeParams.has(param.name)) + ) { + const jsonObject: Partial = {}; + member.serializeInto(jsonObject); + member = deserializerModule.Deserializer.deserialize(context, { + ...jsonObject, + typeParameters: (jsonObject as IApiTypeParameterListMixinJson).typeParameters.map( + ({ typeParameterName, constraintTokenRange, defaultTypeTokenRange }) => ({ + typeParameterName: typeParams.get(typeParameterName) ?? typeParameterName, + defaultTypeTokenRange, + constraintTokenRange, + }), + ), + } as IApiTypeParameterListMixinJson); + } + + if (ApiReturnTypeMixin.isBaseClassOf(member)) { + const jsonObject: Partial = {}; + member.serializeInto(jsonObject); + member = deserializerModule.Deserializer.deserialize(context, { + ...(jsonObject as IApiReturnTypeMixinJson), + excerptTokens: (jsonObject as IApiDeclaredItemJson).excerptTokens.map((token) => + token.kind === ExcerptTokenKind.Content + ? { + kind: ExcerptTokenKind.Content, + text: [...typeParams.keys()].reduce( + (tok, typ) => tok.replaceAll(new RegExp(`\b${typ}\b`, 'g'), typeParams.get(typ)!), + token.text, + ), + } + : token, + ), + } as IApiReturnTypeMixinJson); + member[apiItem_onParentChanged](next.item); + } // */ + membersToAdd.push(member); } } else if (!membersByKind.has(member.kind)) { @@ -336,18 +395,18 @@ export function ApiItemContainerMixin( } // Interfaces can extend multiple interfaces, so iterate through all of them. - const extendedItems: ApiItem[] = []; + const extendedItems: IMappedTypeParameters[] = []; let extendsTypes: readonly HeritageType[] | undefined; - switch (next.kind) { + switch (next.item.kind) { case ApiItemKind.Class: { - const apiClass: ApiClass = next as ApiClass; + const apiClass: ApiClass = next.item as ApiClass; extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : []; break; } case ApiItemKind.Interface: { - const apiInterface: ApiInterface = next as ApiInterface; + const apiInterface: ApiInterface = next.item as ApiInterface; extendsTypes = apiInterface.extendsTypes; break; } @@ -359,7 +418,7 @@ export function ApiItemContainerMixin( if (extendsTypes === undefined) { messages.push({ messageId: FindApiItemsMessageId.UnsupportedKind, - text: `Unable to analyze references of API item ${next.displayName} because it is of unsupported kind ${next.kind}`, + text: `Unable to analyze references of API item ${next.item.displayName} because it is of unsupported kind ${next.item.kind}`, }); maybeIncompleteResult = true; next = toVisit.shift(); @@ -387,7 +446,7 @@ export function ApiItemContainerMixin( if (!firstReferenceToken) { messages.push({ messageId: FindApiItemsMessageId.ExtendsClauseMissingReference, - text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.displayName} because no canonical reference was found`, + text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`, }); maybeIncompleteResult = true; continue; @@ -397,7 +456,7 @@ export function ApiItemContainerMixin( if (!apiModel) { messages.push({ messageId: FindApiItemsMessageId.NoAssociatedApiModel, - text: `Unable to analyze references of API item ${next.displayName} because it is not associated with an ApiModel`, + text: `Unable to analyze references of API item ${next.item.displayName} because it is not associated with an ApiModel`, }); maybeIncompleteResult = true; continue; @@ -413,13 +472,24 @@ export function ApiItemContainerMixin( if (!apiItem) { messages.push({ messageId: FindApiItemsMessageId.DeclarationResolutionFailed, - text: `Unable to resolve declaration reference within API item ${next.displayName}: ${apiItemResult.errorMessage}`, + text: `Unable to resolve declaration reference within API item ${next.item.displayName}: ${apiItemResult.errorMessage}`, }); maybeIncompleteResult = true; continue; } - extendedItems.push(apiItem); + const mappedTypeParameters: Map = new Map(); + if ( + (apiItem.kind === ApiItemKind.Class || apiItem.kind === ApiItemKind.Interface) && + next.item.kind === ApiItemKind.Class + ) { + for (const [index, typeParameter] of extendsType.typeParameters.entries()) { + const key = (apiItem as ApiClass | ApiInterface).typeParameters[index]?.name ?? ''; + mappedTypeParameters.set(key, typeParameter); + } + } + + extendedItems.push({ item: apiItem, mappedTypeParameters }); } // For classes, this array will only have one item. For interfaces, there may be multiple items. Sort the array @@ -440,7 +510,9 @@ export function ApiItemContainerMixin( // // interface FooBar extends Foo, Bar {} // ``` - extendedItems.sort((x: ApiItem, y: ApiItem) => x.getSortKey().localeCompare(y.getSortKey())); + extendedItems.sort((x: IMappedTypeParameters, y: IMappedTypeParameters) => + x.item.getSortKey().localeCompare(y.item.getSortKey()), + ); toVisit.push(...extendedItems); next = toVisit.shift(); diff --git a/packages/api-extractor-model/src/mixins/Excerpt.ts b/packages/api-extractor-model/src/mixins/Excerpt.ts index 73213637cf74..a7f457f56c07 100644 --- a/packages/api-extractor-model/src/mixins/Excerpt.ts +++ b/packages/api-extractor-model/src/mixins/Excerpt.ts @@ -147,7 +147,11 @@ export class Excerpt { this.tokenRange.endIndex > this.tokens.length || this.tokenRange.startIndex > this.tokenRange.endIndex ) { - throw new Error('Invalid token range'); + throw new Error( + `Invalid token range. length:${this.tokens.length}, start:${this.tokenRange.startIndex}, end:${ + this.tokenRange.endIndex + }, ${this.tokens.map((token) => token.text)}`, + ); } this.spannedTokens = this.tokens.slice(this.tokenRange.startIndex, this.tokenRange.endIndex); diff --git a/packages/api-extractor-model/src/model/ApiClass.ts b/packages/api-extractor-model/src/model/ApiClass.ts index 3c92335fe201..77648458bbab 100644 --- a/packages/api-extractor-model/src/model/ApiClass.ts +++ b/packages/api-extractor-model/src/model/ApiClass.ts @@ -39,8 +39,12 @@ export interface IApiClassOptions IApiDeclaredItemOptions, IApiTypeParameterListMixinOptions, IApiExportedMixinOptions { - extendsTokenRange: IExcerptTokenRange | undefined; - implementsTokenRanges: IExcerptTokenRange[]; + extendsTokenRange: IExcerptTokenRangeWithTypeParameters | undefined; + implementsTokenRanges: IExcerptTokenRangeWithTypeParameters[]; +} + +export interface IExcerptTokenRangeWithTypeParameters extends IExcerptTokenRange { + typeParameters: string[]; } export interface IApiClassJson @@ -48,8 +52,8 @@ export interface IApiClassJson IApiAbstractMixinJson, IApiTypeParameterListMixinJson, IApiExportedMixinJson { - extendsTokenRange?: IExcerptTokenRange; - implementsTokenRanges: IExcerptTokenRange[]; + extendsTokenRange?: IExcerptTokenRangeWithTypeParameters | undefined; + implementsTokenRanges: IExcerptTokenRangeWithTypeParameters[]; } /** @@ -81,13 +85,18 @@ export class ApiClass extends ApiItemContainerMixin( super(options); if (options.extendsTokenRange) { - this.extendsType = new HeritageType(this.buildExcerpt(options.extendsTokenRange)); + this.extendsType = new HeritageType( + this.buildExcerpt(options.extendsTokenRange), + options.extendsTokenRange.typeParameters, + ); } else { this.extendsType = undefined; } for (const implementsTokenRange of options.implementsTokenRanges) { - this._implementsTypes.push(new HeritageType(this.buildExcerpt(implementsTokenRange))); + this._implementsTypes.push( + new HeritageType(this.buildExcerpt(implementsTokenRange), implementsTokenRange.typeParameters), + ); } } @@ -138,10 +147,16 @@ export class ApiClass extends ApiItemContainerMixin( // Note that JSON does not support the "undefined" value, so we simply omit the field entirely if it is undefined if (this.extendsType) { - jsonObject.extendsTokenRange = this.extendsType.excerpt.tokenRange; + jsonObject.extendsTokenRange = { + ...this.extendsType.excerpt.tokenRange, + typeParameters: this.extendsType.typeParameters, + }; } - jsonObject.implementsTokenRanges = this.implementsTypes.map((x) => x.excerpt.tokenRange); + jsonObject.implementsTokenRanges = this.implementsTypes.map((x) => ({ + ...x.excerpt.tokenRange, + typeParameters: x.typeParameters, + })); } /** diff --git a/packages/api-extractor-model/src/model/ApiEvent.ts b/packages/api-extractor-model/src/model/ApiEvent.ts new file mode 100644 index 000000000000..b00668bddb33 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiEvent.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; + +/** + * Constructor options for {@link ApiEvent}. + * + * @public + */ +export interface IApiEventOptions + extends IApiNameMixinOptions, + IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript event declaration that belongs to an `ApiClass`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiEvent` represents a emittable event such as the `ready` event in this example: + * + * ```ts + * export class Cliet extends EventEmitter { + * on(event: 'ready', ...args: [Client]): this { } + * } + * ``` + * @public + */ +export class ApiEvent extends ApiNameMixin(ApiParameterListMixin(ApiReleaseTagMixin(ApiDeclaredItem))) { + public constructor(options: IApiEventOptions) { + super(options); + } + + public static getContainerKey(name: string, overloadIndex: number): string { + return `${name}|${ApiItemKind.Event}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Event; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiEvent.getContainerKey(this.name, this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(Navigation.Members as any, nameComponent) + .withMeaning(Meaning.Member as any) + .withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiInterface.ts b/packages/api-extractor-model/src/model/ApiInterface.ts index 150d03ce369a..b55e5eb2a864 100644 --- a/packages/api-extractor-model/src/model/ApiInterface.ts +++ b/packages/api-extractor-model/src/model/ApiInterface.ts @@ -25,7 +25,7 @@ import { type IApiTypeParameterListMixinJson, ApiTypeParameterListMixin, } from '../mixins/ApiTypeParameterListMixin.js'; -import type { IExcerptTokenRange } from '../mixins/Excerpt.js'; +import type { IExcerptTokenRangeWithTypeParameters } from './ApiClass.js'; import type { DeserializerContext } from './DeserializerContext.js'; import { HeritageType } from './HeritageType.js'; @@ -41,7 +41,7 @@ export interface IApiInterfaceOptions IApiReleaseTagMixinOptions, IApiDeclaredItemOptions, IApiExportedMixinOptions { - extendsTokenRanges: IExcerptTokenRange[]; + extendsTokenRanges: IExcerptTokenRangeWithTypeParameters[]; } export interface IApiInterfaceJson @@ -51,7 +51,7 @@ export interface IApiInterfaceJson IApiReleaseTagMixinJson, IApiDeclaredItemJson, IApiExportedMixinJson { - extendsTokenRanges: IExcerptTokenRange[]; + extendsTokenRanges: IExcerptTokenRangeWithTypeParameters[]; } /** @@ -79,7 +79,7 @@ export class ApiInterface extends ApiItemContainerMixin( super(options); for (const extendsTokenRange of options.extendsTokenRanges) { - this._extendsTypes.push(new HeritageType(this.buildExcerpt(extendsTokenRange))); + this._extendsTypes.push(new HeritageType(this.buildExcerpt(extendsTokenRange), extendsTokenRange.typeParameters)); } } @@ -127,7 +127,10 @@ export class ApiInterface extends ApiItemContainerMixin( public override serializeInto(jsonObject: Partial): void { super.serializeInto(jsonObject); - jsonObject.extendsTokenRanges = this.extendsTypes.map((x) => x.excerpt.tokenRange); + jsonObject.extendsTokenRanges = this.extendsTypes.map((x) => ({ + ...x.excerpt.tokenRange, + typeParameters: x.typeParameters, + })); } /** diff --git a/packages/api-extractor-model/src/model/Deserializer.ts b/packages/api-extractor-model/src/model/Deserializer.ts index 7433748b3af6..53e5ab608174 100644 --- a/packages/api-extractor-model/src/model/Deserializer.ts +++ b/packages/api-extractor-model/src/model/Deserializer.ts @@ -1,9 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { TSDocConfiguration } from '@microsoft/tsdoc'; +import type { IExcerptToken } from '../index.js'; +import { ExcerptTokenKind } from '../index.js'; import type { IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import type { IApiDocumentedItemJson } from '../items/ApiDocumentedItem.js'; import { type IApiItemJson, type IApiItemOptions, type ApiItem, ApiItemKind } from '../items/ApiItem.js'; import type { IApiPropertyItemJson } from '../items/ApiPropertyItem.js'; +import type { IApiAbstractMixinJson } from '../mixins/ApiAbstractMixin.js'; +import type { IApiItemContainerJson } from '../mixins/ApiItemContainerMixin.js'; +import type { IApiNameMixinJson } from '../mixins/ApiNameMixin.js'; +import type { IApiOptionalMixinJson } from '../mixins/ApiOptionalMixin.js'; +import type { IApiParameterListJson, IApiParameterOptions } from '../mixins/ApiParameterListMixin.js'; +import type { IApiProtectedMixinJson } from '../mixins/ApiProtectedMixin.js'; +import type { IApiReadonlyMixinJson } from '../mixins/ApiReadonlyMixin.js'; +import type { IApiReleaseTagMixinJson } from '../mixins/ApiReleaseTagMixin.js'; +import type { IApiReturnTypeMixinJson } from '../mixins/ApiReturnTypeMixin.js'; +import type { IApiStaticMixinJson } from '../mixins/ApiStaticMixin.js'; +import type { IApiTypeParameterListMixinJson } from '../mixins/ApiTypeParameterListMixin.js'; import { ApiCallSignature, type IApiCallSignatureOptions } from './ApiCallSignature.js'; import { ApiClass, type IApiClassOptions, type IApiClassJson } from './ApiClass.js'; import { ApiConstructSignature, type IApiConstructSignatureOptions } from './ApiConstructSignature.js'; @@ -11,6 +26,8 @@ import { ApiConstructor, type IApiConstructorOptions } from './ApiConstructor.js import { ApiEntryPoint, type IApiEntryPointOptions } from './ApiEntryPoint.js'; import { ApiEnum, type IApiEnumOptions } from './ApiEnum.js'; import { ApiEnumMember, type IApiEnumMemberOptions } from './ApiEnumMember.js'; +import type { IApiEventOptions } from './ApiEvent.js'; +import { ApiEvent } from './ApiEvent.js'; import { ApiFunction, type IApiFunctionOptions } from './ApiFunction.js'; import { ApiIndexSignature, type IApiIndexSignatureOptions } from './ApiIndexSignature.js'; import { ApiInterface, type IApiInterfaceOptions, type IApiInterfaceJson } from './ApiInterface.js'; @@ -23,7 +40,313 @@ import { ApiProperty, type IApiPropertyOptions } from './ApiProperty.js'; import { ApiPropertySignature, type IApiPropertySignatureOptions } from './ApiPropertySignature.js'; import { ApiTypeAlias, type IApiTypeAliasOptions, type IApiTypeAliasJson } from './ApiTypeAlias.js'; import { ApiVariable, type IApiVariableOptions, type IApiVariableJson } from './ApiVariable.js'; -import type { DeserializerContext } from './DeserializerContext.js'; +import { ApiJsonSchemaVersion, type DeserializerContext } from './DeserializerContext.js'; + +type DocgenAccess = 'private' | 'protected' | 'public'; +type DocgenScope = 'global' | 'instance' | 'static'; +type DocgenDeprecated = boolean | string; + +interface DocgenMetaJson { + file: string; + line: number; + path: string; +} + +interface DocgenTypeJson { + names?: string[] | undefined; +} + +interface DocgenVarJson { + description?: string; + nullable?: boolean; + types?: string[][][]; +} +type DocgenVarTypeJson = DocgenVarJson | string[][][]; +interface DocgenExceptionJson { + description?: string; + nullable?: boolean; + type: DocgenTypeJson; +} + +interface DocgenExternalJson { + description: string; + meta: DocgenMetaJson; + name: string; + see?: string[]; +} + +interface DocgenTypedefJson { + access?: DocgenAccess; + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + props?: DocgenParamJson[]; + returns?: DocgenVarTypeJson[]; + see?: string[]; + type: DocgenVarTypeJson; +} + +interface DocgenEventJson { + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + see?: string[]; +} + +interface DocgenParamJson { + default?: string; + description: string; + name: string; + nullable?: boolean; + optional?: boolean; + type: DocgenVarTypeJson; + variable?: string; +} + +interface DocgenConstructorJson { + access?: DocgenAccess; + description: string; + name: string; + params?: DocgenParamJson[]; + see?: string[]; +} + +interface DocgenMethodJson { + abstract: boolean; + access?: DocgenAccess; + async?: boolean; + deprecated?: DocgenDeprecated; + description: string; + emits?: string[]; + examples?: string[]; + generator?: boolean; + implements?: string[]; + inherited?: boolean; + inherits?: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + returns?: DocgenVarTypeJson[]; + scope: DocgenScope; + see?: string[]; + throws?: DocgenExceptionJson[]; +} + +interface DocgenPropertyJson { + abstract?: boolean; + access?: DocgenAccess; + default?: string; + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + nullable?: boolean; + props?: DocgenPropertyJson[]; + readonly?: boolean; + scope: DocgenScope; + see?: string[]; + type: DocgenVarTypeJson; +} +interface DocgenClassJson { + abstract?: boolean; + access?: DocgenAccess; + construct: DocgenConstructorJson; + deprecated?: DocgenDeprecated | string; + description: string; + events?: DocgenEventJson[]; + extends?: DocgenVarTypeJson; + implements?: DocgenVarTypeJson; + meta: DocgenMetaJson; + methods?: DocgenMethodJson[]; + name: string; + props?: DocgenPropertyJson[]; + see?: string[]; +} +type DocgenInterfaceJson = DocgenClassJson; + +export interface DocgenJson { + classes: DocgenClassJson[]; + externals: DocgenExternalJson[]; + functions: DocgenMethodJson[]; + interfaces: DocgenInterfaceJson[]; + meta: { + date: number; + format: number; + generator: string; + }; + typedefs: DocgenTypedefJson[]; +} + +function formatVarType(type: DocgenVarTypeJson): string { + return (Array.isArray(type) ? type : type.types ?? []).map((t1) => t1.map((t2) => t2.join('')).join('')).join(' | '); +} + +function getFirstType(type: DocgenVarTypeJson): string { + return (Array.isArray(type) ? type[0]?.[0]?.[0] : type.types?.[0]?.[0]?.[0]) ?? 'unknown'; +} + +// function mapEvent(_event: DocgenEventJson, _package: string, _parent: DocgenClassJson): void {} + +function mapVarType(type: DocgenVarTypeJson, _package: string): IExcerptToken[] { + const mapper = Array.isArray(type) ? type : type.types ?? []; + return mapper.flatMap((typ) => + typ.reduce( + (arr, [_class, symbol]) => [ + ...arr, + { + kind: ExcerptTokenKind.Reference, + text: _class ?? 'unknown', + canonicalReference: `${_package}!${_class}:class`, + }, + { kind: ExcerptTokenKind.Content, text: symbol ?? '' }, + ], + [], + ), + ); +} + +function mapProp( + prop: DocgenPropertyJson, + _package: string, + parent: DocgenClassJson | DocgenInterfaceJson, +): IApiNameMixinJson & IApiOptionalMixinJson & IApiPropertyItemJson & IApiReadonlyMixinJson & IApiReleaseTagMixinJson { + const mappedVarType = mapVarType(prop.type, _package); + return { + kind: ApiItemKind.Property, + name: prop.name, + isOptional: Boolean(prop.nullable), + isReadonly: Boolean(prop.readonly), + docComment: `/**\n * ${prop.description}\n${prop.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${ + prop.readonly ? ' * @readonly\n' : '' + } */`, + excerptTokens: [ + { + kind: ExcerptTokenKind.Content, + text: `${prop.access} ${prop.scope === 'static' ? 'static ' : ''}${prop.readonly ? 'readonly ' : ''}${ + prop.name + } :`, + }, + ...mappedVarType, + { + kind: ExcerptTokenKind.Reference, + text: formatVarType(prop.type), + canonicalReference: `${_package}!${getFirstType(prop.type)}:class`, + }, + { kind: ExcerptTokenKind.Content, text: ';' }, + ], + propertyTypeTokenRange: { startIndex: 1, endIndex: 1 + mappedVarType.length }, + canonicalReference: `${_package}!${parent.name}#${prop.name}:member`, + releaseTag: prop.access === 'public' ? 'Public' : 'Internal', + fileLine: prop.meta.line, + fileUrlPath: `${prop.meta.path.slice(`packages/${_package}/`.length)}/${prop.meta.file}`, + }; +} + +function mapParam( + param: DocgenParamJson, + index: number, + _package: string, + paramTokens: number[], +): IApiParameterOptions { + return { + parameterName: param.name.startsWith('...') ? param.name.slice(3) : param.name, + isOptional: Boolean(param.optional), + isRest: param.name.startsWith('...'), + parameterTypeTokenRange: { + startIndex: 1 + index + paramTokens.slice(0, index).reduce((akk, num) => akk + num, 0), + endIndex: 1 + index + paramTokens.slice(0, index + 1).reduce((akk, num) => akk + num, 0), + }, + }; +} + +interface IApiMethodJson + extends IApiAbstractMixinJson, + IApiDeclaredItemJson, + IApiNameMixinJson, + IApiOptionalMixinJson, + IApiParameterListJson, + IApiProtectedMixinJson, + IApiReleaseTagMixinJson, + IApiReturnTypeMixinJson, + IApiStaticMixinJson, + IApiTypeParameterListMixinJson {} + +interface IApiConstructorJson + extends IApiParameterListJson, + IApiProtectedMixinJson, + IApiReleaseTagMixinJson, + IApiDeclaredItemJson {} + +function mapMethod(method: DocgenMethodJson, _package: string, parent?: DocgenClassJson): IApiMethodJson { + const excerptTokens: IExcerptToken[] = []; + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: `${ + method.scope === 'global' + ? `export function ${method.name}(` + : `${method.access}${method.scope === 'static' ? ' static' : ''} ${method.name}(` + }${ + method.params?.length + ? `${method.params[0]!.name}${method.params[0]!.nullable || method.params[0]!.optional ? '?' : ''}` + : '): ' + }`, + }); + const paramTokens: number[] = []; + for (let index = 0; index < (method.params?.length ?? 0) - 1; index++) { + const newTokens = mapVarType(method.params![index]!.type, _package); + paramTokens.push(newTokens.length); + excerptTokens.push(...newTokens); + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: `, ${method.params![index + 1]!.name}${ + method.params![index + 1]!.nullable || method.params![index + 1]!.optional ? '?' : '' + }: `, + }); + } + + if (method.params?.length) { + const newTokens = mapVarType(method.params[method.params.length - 1]!.type, _package); + paramTokens.push(newTokens.length); + excerptTokens.push(...newTokens); + excerptTokens.push({ kind: ExcerptTokenKind.Content, text: `): ` }); + } + + const returnTokens = mapVarType(method.returns?.[0] ?? [], _package); + excerptTokens.push(...returnTokens); + + excerptTokens.push({ kind: ExcerptTokenKind.Content, text: ';' }); + + return { + kind: parent ? ApiItemKind.Method : ApiItemKind.Function, + name: method.name, + isAbstract: method.abstract, + isOptional: false, + isProtected: method.access === 'protected', + isStatic: method.scope === 'static', + canonicalReference: `${_package}!${parent ? `${parent.name}!${method.name}:member` : `${method.name}:function`}`, + overloadIndex: 1, + parameters: method.params?.map((param, index) => mapParam(param, index, _package, paramTokens)) ?? [], + releaseTag: method.access === 'public' ? 'Public' : 'Internal', + returnTypeTokenRange: method.returns?.length + ? method.params?.length + ? { startIndex: 2 + 2 * method.params.length, endIndex: 3 + 2 * method.params.length } + : { startIndex: 1, endIndex: 2 } + : { startIndex: 0, endIndex: 0 }, + typeParameters: [], + docComment: `/**\n * ${method.description}\n${ + method.params?.map((param) => ` * @param ${param.name} - ${param.description}\n`).join('') ?? '' + }${ + method.returns?.length && !Array.isArray(method.returns[0]) ? ` * @returns ${method.returns[0]!.description}` : '' + } */`, + excerptTokens, + fileLine: method.meta.line, + fileUrlPath: `${method.meta.path.slice(`packages/${_package}/`.length)}/${method.meta.file}`, + }; +} export class Deserializer { public static deserialize(context: DeserializerContext, jsonObject: IApiItemJson): ApiItem { @@ -51,6 +374,9 @@ export class Deserializer { case ApiItemKind.EnumMember: ApiEnumMember.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); return new ApiEnumMember(options as IApiEnumMemberOptions); + case ApiItemKind.Event: + ApiEvent.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiEvent(options as IApiEventOptions); case ApiItemKind.Function: ApiFunction.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); return new ApiFunction(options as IApiFunctionOptions); @@ -90,4 +416,162 @@ export class Deserializer { throw new Error(`Failed to deserialize unsupported API item type ${JSON.stringify(jsonObject.kind)}`); } } + + public static deserializeDocgen(jsonObject: DocgenJson, _package: string) { + const context: DeserializerContext = { + apiJsonFilename: 'docs.json', + tsdocConfiguration: new TSDocConfiguration(), + versionToDeserialize: ApiJsonSchemaVersion.V_1011, + toolPackage: jsonObject.meta.generator, + toolVersion: jsonObject.meta.format.toString(), + }; + + let members: (IApiClassJson | IApiInterfaceJson | IApiMethodJson | IApiTypeAliasJson)[] = []; + + for (const _class of jsonObject.classes) { + const classMembers: (IApiConstructorJson | IApiMethodJson | IApiPropertyItemJson)[] = [ + // ..._class.events.map((event) => mapEvent(event, _package, _class)), + ...(_class.props?.map((prop) => mapProp(prop, _package, _class)) ?? []), + ...(_class.methods?.map((method) => mapMethod(method, _package, _class)) ?? []), + ]; + if (_class.construct) { + const excerptTokens: IExcerptToken[] = [ + { + kind: ExcerptTokenKind.Content, + text: `${_class.construct.access} constructor(${ + _class.construct.params?.length ? `${_class.construct.params[0]?.name}: ` : ');' + }`, + }, + ]; + + const paramTokens: number[] = []; + for (let index = 0; index < (_class.construct.params?.length ?? 0) - 1; index++) { + const newTokens = mapVarType(_class.construct.params![index]!.type, _package); + paramTokens.push(newTokens.length); + excerptTokens.push(...newTokens); + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: `, ${_class.construct.params![index + 1]?.name}: `, + }); + } + + if (_class.construct.params?.length) { + const newTokens = mapVarType(_class.construct.params[_class.construct.params.length - 1]!.type, _package); + paramTokens.push(newTokens.length); + excerptTokens.push(...newTokens); + excerptTokens.push({ kind: ExcerptTokenKind.Content, text: ');' }); + } + + classMembers.unshift({ + parameters: + _class.construct.params?.map((param, index) => mapParam(param, index, _package, paramTokens)) ?? [], + isProtected: _class.construct.access === 'protected', + releaseTag: _class.construct.access === 'public' ? 'Public' : 'Internal', + docComment: `/*+\n * ${_class.construct.description}\n${ + _class.construct.params?.map((param) => ` * @param ${param.name} - ${param.description}\n`).join('') ?? '' + } */`, + excerptTokens, + kind: ApiItemKind.Constructor, + canonicalReference: `${_package}!${_class.name}:constructor`, + overloadIndex: 0, + }); + } + + const excerptTokens: IExcerptToken[] = [ + { + kind: ExcerptTokenKind.Content, + text: `${_class.access === 'public' ? 'export ' : ''}declare class ${_class.name}${ + _class.extends ? ' extends ' : _class.implements ? ' implements ' : '' + }`, + }, + ]; + + if (_class.extends) + excerptTokens.push({ + kind: ExcerptTokenKind.Reference, + text: formatVarType(_class.extends) ?? '', + canonicalReference: `${_package}!${getFirstType(_class.extends) ?? ''}:class`, + }); + + if (_class.extends && _class.implements) + excerptTokens.push({ kind: ExcerptTokenKind.Content, text: ' implements ' }); + + if (_class.implements) + excerptTokens.push({ + kind: ExcerptTokenKind.Reference, + text: formatVarType(_class.implements) ?? '', + canonicalReference: `${_package}!${getFirstType(_class.implements) ?? ''}:class`, + }); + + members.push({ + members: classMembers, + kind: ApiItemKind.Class, + canonicalReference: `${_package}!${_class.name}:class`, + name: _class.name, + extendsTokenRange: _class.extends ? { startIndex: 1, endIndex: 2, typeParameters: [] } : undefined, + excerptTokens, + implementsTokenRanges: _class.implements + ? [{ startIndex: _class.extends ? 3 : 1, endIndex: _class.extends ? 4 : 2, typeParameters: [] }] + : [], + typeParameters: [], + releaseTag: _class.access === 'public' ? 'Public' : 'Internal', + docComment: `/**\n * ${_class.description}\n${_class.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''} */`, + isExported: _class.access === 'public', + isAbstract: Boolean(_class.abstract), + fileLine: _class.meta.line, + fileUrlPath: `${_class.meta.path.slice(`packages/${_package}/`.length)}/${_class.meta.file}`, + }); + } + + members = [ + ...members, + ...jsonObject.functions.map((_func) => mapMethod(_func, _package)), + ...jsonObject.interfaces.map((_interface) => ({ + members: [ + ...(_interface.props?.map((prop) => mapProp(prop, _package, _interface)) ?? []), + ...(_interface.methods?.map((method) => mapMethod(method, _package, _interface)) ?? []), + ], + kind: ApiItemKind.Interface, + canonicalReference: `${_package}!${_interface.name}:interface`, + name: _interface.name, + extendsTokenRanges: [{ startIndex: 0, endIndex: 0, typeParameters: [] }], + excerptTokens: [ + { + kind: ExcerptTokenKind.Content, + text: `${_interface.access === 'public' ? 'export ' : ''}interface ${_interface.name}`, + }, + ], + typeParameters: [], + releaseTag: _interface.access === 'public' ? 'Public' : 'Internal', + docComment: `/**\n * ${_interface.description}\n${ + _interface.see?.map((see) => ` * @see ${see}\n`).join('') ?? '' + } */`, + isExported: _interface.access === 'public', + fileLine: _interface.meta.line, + fileUrlPath: `${_interface.meta.path.slice(`packages/${_package}/`.length)}/${_interface.meta.file}`, + })), + ]; + + const reworkedJson: IApiDocumentedItemJson & + IApiItemContainerJson & + IApiNameMixinJson & + IApiPackageJson & { members: (IApiItemContainerJson & IApiNameMixinJson)[] } = { + projectFolderUrl: `https://github.com/discordjs/discord.js/tree/main/packages/${_package}`, + metadata: { ...context, tsdocConfig: context.tsdocConfiguration, schemaVersion: context.versionToDeserialize }, + canonicalReference: `!${_package}`, + kind: ApiItemKind.Package, + name: _package, + members: [ + { + members, + name: _package, + kind: ApiItemKind.EntryPoint, + canonicalReference: `${_package}!`, + }, + ], + docComment: '', + }; + + return Deserializer.deserialize(context, reworkedJson); + } } diff --git a/packages/api-extractor-model/src/model/HeritageType.ts b/packages/api-extractor-model/src/model/HeritageType.ts index a4fcfa2d9247..a3f319d927c6 100644 --- a/packages/api-extractor-model/src/model/HeritageType.ts +++ b/packages/api-extractor-model/src/model/HeritageType.ts @@ -38,7 +38,10 @@ export class HeritageType { */ public readonly excerpt: Excerpt; - public constructor(excerpt: Excerpt) { + public readonly typeParameters: string[]; + + public constructor(excerpt: Excerpt, typeParameters: string[]) { this.excerpt = excerpt; + this.typeParameters = typeParameters; } } diff --git a/packages/api-extractor/README.md b/packages/api-extractor/README.md index 734576b9e3bb..5c34980c70f3 100644 --- a/packages/api-extractor/README.md +++ b/packages/api-extractor/README.md @@ -1,4 +1,4 @@ -# @microsoft/api-extractor +# @discordjs/api-extractor ![API Extractor](https://github.com/microsoft/rushstack/raw/main/common/wiki-images/api-extractor-title.png?raw=true)
@@ -42,7 +42,7 @@ For more details and support resources, please visit: https://api-extractor.com/ ## Links -- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - Find +- [CHANGELOG.md](https://github.com/discordjs/discord.js/blob/main/packages/api-extractor/CHANGELOG.md) - Find out what's new in the latest version - [API Reference](https://rushstack.io/pages/api/api-extractor/) diff --git a/packages/api-extractor/extends/tsdoc-base.json b/packages/api-extractor/extends/tsdoc-base.json index 119244c55e4f..38dce1501a25 100644 --- a/packages/api-extractor/extends/tsdoc-base.json +++ b/packages/api-extractor/extends/tsdoc-base.json @@ -7,7 +7,7 @@ * ``` * { * "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", - * "extends": [ "@microsoft/api-extractor/extends/tsdoc-config.json" ], + * "extends": [ "@discordjs/api-extractor/extends/tsdoc-config.json" ], * . . . * } * ``` diff --git a/packages/api-extractor/src/analyzer/PackageMetadataManager.ts b/packages/api-extractor/src/analyzer/PackageMetadataManager.ts index 26d52bd0680a..2fc889fb2f31 100644 --- a/packages/api-extractor/src/analyzer/PackageMetadataManager.ts +++ b/packages/api-extractor/src/analyzer/PackageMetadataManager.ts @@ -131,7 +131,7 @@ export class PackageMetadataManager { tsdocVersion: '0.12', toolPackages: [ { - packageName: '@microsoft/api-extractor', + packageName: '@discordjs/api-extractor', packageVersion: Extractor.version, }, ], diff --git a/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts b/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts index c3d856feb01c..4527e27a1fda 100644 --- a/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts +++ b/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts @@ -121,5 +121,3 @@ describe(PackageMetadataManager.name, () => { }); }); }); - -/* eslint-enable @typescript-eslint/typedef */ diff --git a/packages/api-extractor/src/api/ExtractorConfig.ts b/packages/api-extractor/src/api/ExtractorConfig.ts index 3a058cd96d27..2a470aa40c0e 100644 --- a/packages/api-extractor/src/api/ExtractorConfig.ts +++ b/packages/api-extractor/src/api/ExtractorConfig.ts @@ -142,7 +142,7 @@ export interface IExtractorConfigPrepareOptions { /** * Allow customization of the tsdoc.json config file. If omitted, this file will be loaded from its default * location. If the file does not exist, then the standard definitions will be used from - * `@microsoft/api-extractor/extends/tsdoc-base.json`. + * `@discordjs/api-extractor/extends/tsdoc-base.json`. */ tsdocConfigFile?: TSDocConfigFile; } diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts index b30d46ccd960..94a8f7aa7177 100644 --- a/packages/api-extractor/src/generators/ApiModelGenerator.ts +++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { existsSync } from 'node:fs'; import * as path from 'node:path'; import { ApiModel, ApiClass, ApiPackage, ApiEntryPoint, + ApiEvent, ApiMethod, ApiNamespace, ApiInterface, @@ -19,6 +21,7 @@ import { ApiEnum, ApiEnumMember, type IExcerptTokenRange, + type IExcerptTokenRangeWithTypeParameters, type IExcerptToken, ApiConstructor, ApiConstructSignature, @@ -29,12 +32,17 @@ import { ApiCallSignature, type IApiTypeParameterOptions, EnumMemberOrder, + ExcerptTokenKind, + Navigation, } from '@discordjs/api-extractor-model'; import type * as tsdoc from '@microsoft/tsdoc'; -import { Path } from '@rushstack/node-core-library'; +import { TSDocParser } from '@microsoft/tsdoc'; +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { JsonFile, Path } from '@rushstack/node-core-library'; import * as ts from 'typescript'; import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; import type { AstEntity } from '../analyzer/AstEntity.js'; +import type { AstImport } from '../analyzer/AstImport.js'; import type { AstModule } from '../analyzer/AstModule.js'; import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; import { AstSymbol } from '../analyzer/AstSymbol.js'; @@ -46,10 +54,165 @@ import type { ISourceLocation } from '../collector/SourceMapper.js'; import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator.js'; import { ExcerptBuilder, type IExcerptBuilderNodeToCapture } from './ExcerptBuilder.js'; +type DocgenAccess = 'private' | 'protected' | 'public'; +type DocgenScope = 'global' | 'instance' | 'static'; +type DocgenDeprecated = boolean | string; + +interface DocgenMetaJson { + file: string; + line: number; + path: string; +} + +interface DocgenTypeJson { + names?: string[] | undefined; +} + +interface DocgenVarJson { + description?: string; + nullable?: boolean; + types?: string[][][]; +} +type DocgenVarTypeJson = DocgenVarJson | string[][][]; +interface DocgenExceptionJson { + description?: string; + nullable?: boolean; + type: DocgenTypeJson; +} + +interface DocgenExternalJson { + description: string; + meta: DocgenMetaJson; + name: string; + see?: string[]; +} + +interface DocgenTypedefJson { + access?: DocgenAccess; + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + props?: DocgenParamJson[]; + returns?: DocgenVarTypeJson[]; + see?: string[]; + type: DocgenVarTypeJson; +} + +interface DocgenEventJson { + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + see?: string[]; +} + +interface DocgenParamJson { + default?: string; + description: string; + name: string; + nullable?: boolean; + optional?: boolean; + type: DocgenVarTypeJson; + variable?: string; +} + +interface DocgenConstructorJson { + access?: DocgenAccess; + description: string; + name: string; + params?: DocgenParamJson[]; + see?: string[]; +} + +interface DocgenMethodJson { + abstract: boolean; + access?: DocgenAccess; + async?: boolean; + deprecated?: DocgenDeprecated; + description: string; + emits?: string[]; + examples?: string[]; + generator?: boolean; + implements?: string[]; + inherited?: boolean; + inherits?: string; + meta: DocgenMetaJson; + name: string; + params?: DocgenParamJson[]; + returns?: DocgenVarTypeJson[]; + scope: DocgenScope; + see?: string[]; + throws?: DocgenExceptionJson[]; +} + +interface DocgenPropertyJson { + abstract?: boolean; + access?: DocgenAccess; + default?: string; + deprecated?: DocgenDeprecated; + description: string; + meta: DocgenMetaJson; + name: string; + nullable?: boolean; + props?: DocgenPropertyJson[]; + readonly?: boolean; + scope: DocgenScope; + see?: string[]; + type: DocgenVarTypeJson; +} +interface DocgenClassJson { + abstract?: boolean; + access?: DocgenAccess; + construct: DocgenConstructorJson; + deprecated?: DocgenDeprecated | string; + description: string; + events?: DocgenEventJson[]; + extends?: DocgenVarTypeJson; + implements?: DocgenVarTypeJson; + meta: DocgenMetaJson; + methods?: DocgenMethodJson[]; + name: string; + props?: DocgenPropertyJson[]; + see?: string[]; +} +type DocgenInterfaceJson = DocgenClassJson; +type DocgenContainerJson = + | DocgenClassJson + | DocgenConstructorJson + | DocgenInterfaceJson + | DocgenJson + | DocgenMethodJson + | DocgenTypedefJson; + +export interface DocgenJson { + classes: DocgenClassJson[]; + externals: DocgenExternalJson[]; + functions: DocgenMethodJson[]; + interfaces: DocgenInterfaceJson[]; + meta: { + date: number; + format: number; + generator: string; + }; + typedefs: DocgenTypedefJson[]; +} interface IProcessAstEntityContext { isExported: boolean; name: string; parentApiItem: ApiItemContainerMixin; + parentDocgenJson?: DocgenContainerJson | undefined; +} + +const linkRegEx = /{@link\s(?\w+)#(?event:)?(?[\w()]+)(?\s[^}]*)?}/g; +function fixLinkTags(input?: string): string | undefined { + return input?.replaceAll(linkRegEx, '{@link $.$$}'); +} + +function filePathFromJson(meta: DocgenMetaJson): string { + return `${meta.path.slice('packages/discord.js/'.length)}/${meta.file}`; } export class ApiModelGenerator { @@ -71,6 +234,12 @@ export class ApiModelGenerator { public buildApiPackage(): ApiPackage { const packageDocComment: tsdoc.DocComment | undefined = this._collector.workingPackage.tsdocComment; + let jsDocJson: DocgenJson | undefined; + + const jsDocFilepath = `${this._collector.extractorConfig.apiJsonFilePath.slice(0, -8)}json`; + if (existsSync(jsDocFilepath)) { + jsDocJson = JsonFile.load(jsDocFilepath); + } const apiPackage: ApiPackage = new ApiPackage({ name: this._collector.workingPackage.name, @@ -92,6 +261,7 @@ export class ApiModelGenerator { name: entity.nameForEmit!, isExported: entity.exportedFromEntryPoint, parentApiItem: apiEntryPoint, + parentDocgenJson: jsDocJson, }); } } @@ -320,6 +490,7 @@ export class ApiModelGenerator { const constructorDeclaration: ts.ConstructorDeclaration = astDeclaration.declaration as ts.ConstructorDeclaration; const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined; const parameters: IApiParameterOptions[] = this._captureParameters( nodesToCapture, @@ -328,7 +499,15 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = parent?.construct + ? new TSDocParser().parseString( + `/*+\n * ${fixLinkTags(parent.construct.description)}\n${ + parent.construct.params + ?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`) + .join('') ?? '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; const sourceLocation: ISourceLocation = this._getSourceLocation(constructorDeclaration); @@ -340,8 +519,8 @@ export class ApiModelGenerator { parameters, overloadIndex, excerptTokens, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: parent ? filePathFromJson(parent.meta) : sourceLocation.sourceFilePath, + fileLine: parent?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -354,9 +533,14 @@ export class ApiModelGenerator { const containerKey: string = ApiClass.getContainerKey(name); let apiClass: ApiClass | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiClass; + const parent = context.parentDocgenJson as DocgenJson | undefined; + const jsDoc = parent?.classes.find((_class) => _class.name === name); if (apiClass === undefined) { const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration; + if (name === 'ActionRow') { + console.dir(classDeclaration.heritageClauses?.[0]?.types[0]?.typeArguments, { depth: 3 }); + } const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; @@ -365,18 +549,29 @@ export class ApiModelGenerator { classDeclaration.typeParameters, ); - let extendsTokenRange: IExcerptTokenRange | undefined; - const implementsTokenRanges: IExcerptTokenRange[] = []; + let extendsTokenRange: IExcerptTokenRangeWithTypeParameters | undefined; + const implementsTokenRanges: IExcerptTokenRangeWithTypeParameters[] = []; for (const heritageClause of classDeclaration.heritageClauses ?? []) { if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { - extendsTokenRange = ExcerptBuilder.createEmptyTokenRange(); + extendsTokenRange = ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); if (heritageClause.types.length > 0) { + extendsTokenRange.typeParameters.push( + ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => + ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', + ) ?? []), + ); nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange }); } } else if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { for (const heritageType of heritageClause.types) { - const implementsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + const implementsTokenRange: IExcerptTokenRangeWithTypeParameters = + ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); + implementsTokenRange.typeParameters.push( + ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => + ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', + ) ?? []), + ); implementsTokenRanges.push(implementsTokenRange); nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange }); } @@ -385,7 +580,17 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${ + jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isAbstract: boolean = (ts.getCombinedModifierFlags(classDeclaration) & ts.ModifierFlags.Abstract) !== 0; const sourceLocation: ISourceLocation = this._getSourceLocation(classDeclaration); @@ -400,8 +605,8 @@ export class ApiModelGenerator { extendsTokenRange, implementsTokenRanges, isExported, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -411,7 +616,12 @@ export class ApiModelGenerator { this._processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiClass, + parentDocgenJson: jsDoc, }); + + for (const event of jsDoc?.events ?? []) { + this._processApiEvent({ ...context, name: event.name, parentApiItem: apiClass, parentDocgenJson: jsDoc }); + } } private _processApiConstructSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { @@ -428,6 +638,7 @@ export class ApiModelGenerator { astDeclaration.declaration as ts.ConstructSignatureDeclaration; const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined; const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); nodesToCapture.push({ node: constructSignature.type, tokenRange: returnTypeTokenRange }); @@ -441,7 +652,15 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = parent?.construct + ? new TSDocParser().parseString( + `/*+\n * ${fixLinkTags(parent.construct.description)}\n${ + parent.construct.params + ?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`) + .join('') ?? '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const sourceLocation: ISourceLocation = this._getSourceLocation(constructSignature); @@ -453,8 +672,8 @@ export class ApiModelGenerator { overloadIndex, excerptTokens, returnTypeTokenRange, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: parent ? filePathFromJson(parent.meta) : sourceLocation.sourceFilePath, + fileLine: parent?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -541,6 +760,8 @@ export class ApiModelGenerator { const containerKey: string = ApiFunction.getContainerKey(name, overloadIndex); let apiFunction: ApiFunction | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiFunction; + const parent = context.parentDocgenJson as DocgenJson | undefined; + const jsDoc = parent?.functions.find((fun) => fun.name === name); if (apiFunction === undefined) { const functionDeclaration: ts.FunctionDeclaration = astDeclaration.declaration as ts.FunctionDeclaration; @@ -562,7 +783,24 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ?? + '' + }${ + jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0]) + ? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}` + : '' + }${ + jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const sourceLocation: ISourceLocation = this._getSourceLocation(functionDeclaration); @@ -576,8 +814,8 @@ export class ApiModelGenerator { excerptTokens, returnTypeTokenRange, isExported, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -633,6 +871,9 @@ export class ApiModelGenerator { const containerKey: string = ApiInterface.getContainerKey(name); let apiInterface: ApiInterface | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiInterface; + const parent = context.parentDocgenJson as DocgenJson | undefined; + const jsDoc = + parent?.interfaces.find((int) => int.name === name) ?? parent?.typedefs.find((int) => int.name === name); if (apiInterface === undefined) { const interfaceDeclaration: ts.InterfaceDeclaration = astDeclaration.declaration as ts.InterfaceDeclaration; @@ -644,12 +885,18 @@ export class ApiModelGenerator { interfaceDeclaration.typeParameters, ); - const extendsTokenRanges: IExcerptTokenRange[] = []; + const extendsTokenRanges: IExcerptTokenRangeWithTypeParameters[] = []; for (const heritageClause of interfaceDeclaration.heritageClauses ?? []) { if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { for (const heritageType of heritageClause.types) { - const extendsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + const extendsTokenRange: IExcerptTokenRangeWithTypeParameters = + ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); + extendsTokenRange.typeParameters.push( + ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => + ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', + ) ?? []), + ); extendsTokenRanges.push(extendsTokenRange); nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange }); } @@ -658,7 +905,17 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${ + jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const sourceLocation: ISourceLocation = this._getSourceLocation(interfaceDeclaration); @@ -670,8 +927,8 @@ export class ApiModelGenerator { typeParameters, extendsTokenRanges, isExported, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -681,6 +938,7 @@ export class ApiModelGenerator { this._processChildDeclarations(astDeclaration, { ...context, parentApiItem: apiInterface, + parentDocgenJson: jsDoc, }); } @@ -691,6 +949,8 @@ export class ApiModelGenerator { const containerKey: string = ApiMethod.getContainerKey(name, isStatic, overloadIndex); let apiMethod: ApiMethod | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiMethod; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined; + const jsDoc = parent?.methods?.find((method) => method.name === name); if (apiMethod === undefined) { const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration; @@ -709,7 +969,24 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ?? + '' + }${ + jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0]) + ? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}` + : '' + }${ + jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { return; // trim out items marked as "@internal" or "@alpha" @@ -733,8 +1010,8 @@ export class ApiModelGenerator { overloadIndex, excerptTokens, returnTypeTokenRange, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -750,6 +1027,8 @@ export class ApiModelGenerator { let apiMethodSignature: ApiMethodSignature | undefined = parentApiItem.tryGetMemberByKey( containerKey, ) as ApiMethodSignature; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined; + const jsDoc = parent?.methods?.find((method) => method.name === name); if (apiMethodSignature === undefined) { const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature; @@ -768,7 +1047,24 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ?? + '' + }${ + jsDoc.returns?.length && !Array.isArray(jsDoc.returns[0]) + ? ` * @returns ${fixLinkTags(jsDoc.returns[0]!.description ?? '')}` + : '' + }${ + jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const sourceLocation: ISourceLocation = this._getSourceLocation(methodSignature); @@ -783,8 +1079,8 @@ export class ApiModelGenerator { overloadIndex, excerptTokens, returnTypeTokenRange, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -830,6 +1126,8 @@ export class ApiModelGenerator { const containerKey: string = ApiProperty.getContainerKey(name, isStatic); let apiProperty: ApiProperty | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiProperty; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | DocgenTypedefJson | undefined; + const jsDoc = parent?.props?.find((prop) => prop.name === name); if (apiProperty === undefined) { const declaration: ts.Declaration = astDeclaration.declaration; @@ -857,7 +1155,19 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + 'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : '' + }${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${ + 'deprecated' in jsDoc && jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; @@ -877,8 +1187,8 @@ export class ApiModelGenerator { excerptTokens, propertyTypeTokenRange, initializerTokenRange, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); parentApiItem.addMember(apiProperty); @@ -895,6 +1205,8 @@ export class ApiModelGenerator { let apiPropertySignature: ApiPropertySignature | undefined = parentApiItem.tryGetMemberByKey( containerKey, ) as ApiPropertySignature; + const parent = context.parentDocgenJson as DocgenInterfaceJson | DocgenPropertyJson | DocgenTypedefJson | undefined; + const jsDoc = parent?.props?.find((prop) => prop.name === name); if (apiPropertySignature === undefined) { const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature; @@ -906,7 +1218,15 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + 'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : '' + }${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${ + 'deprecated' in jsDoc && jsDoc.deprecated ? ` * @deprecated ${jsDoc.deprecated}\n` : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; const isReadonly: boolean = this._isReadonly(astDeclaration); @@ -920,8 +1240,8 @@ export class ApiModelGenerator { excerptTokens, propertyTypeTokenRange, isReadonly, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -938,6 +1258,11 @@ export class ApiModelGenerator { const containerKey: string = ApiTypeAlias.getContainerKey(name); let apiTypeAlias: ApiTypeAlias | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiTypeAlias; + const parent = context.parentDocgenJson as DocgenJson | undefined; + const jsDoc = + parent?.typedefs.find((type) => type.name === name) ?? + parent?.functions.find((func) => func.name === name) ?? + parent?.interfaces.find((clas) => clas.name === name); if (apiTypeAlias === undefined) { const typeAliasDeclaration: ts.TypeAliasDeclaration = astDeclaration.declaration as ts.TypeAliasDeclaration; @@ -954,7 +1279,19 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); - const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const docComment: tsdoc.DocComment | undefined = jsDoc + ? new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description) ?? ''}\n${ + 'params' in jsDoc + ? jsDoc.params.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') + : '' + }${ + 'returns' in jsDoc + ? jsDoc.returns.map((ret) => ` * @returns ${Array.isArray(ret) ? '' : fixLinkTags(ret.description)}\n`) + : '' + } */`, + ).docComment + : apiItemMetadata.tsdocComment; const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; const sourceLocation: ISourceLocation = this._getSourceLocation(typeAliasDeclaration); @@ -966,8 +1303,8 @@ export class ApiModelGenerator { excerptTokens, typeTokenRange, isExported, - fileUrlPath: sourceLocation.sourceFilePath, - fileLine: sourceLocation.sourceFileLine, + fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath, + fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine, fileColumn: sourceLocation.sourceFileColumn, }); @@ -1021,6 +1358,94 @@ export class ApiModelGenerator { } } + // events aren't part of typescript, we only get them from docgen JSON here + private _processApiEvent(context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const containerKey: string = ApiProperty.getContainerKey(name, false); + + let apiEvent: ApiEvent | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiEvent; + const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined; + const jsDoc = parent?.events?.find((prop) => prop.name === name); + + if (apiEvent === undefined && jsDoc) { + const excerptTokens: IExcerptToken[] = [ + { + kind: ExcerptTokenKind.Content, + text: `on('${name}', (${ + jsDoc.params?.length ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.nullable ? '?' : ''}: ` : ') => {})' + }`, + }, + ]; + const parameters: IApiParameterOptions[] = []; + for (let index = 0; index < (jsDoc.params?.length ?? 0) - 1; index++) { + const parameter = jsDoc.params![index]!; + const newTokens = this._mapVarType(parameter.type); + parameters.push({ + parameterName: parameter.name, + parameterTypeTokenRange: { + startIndex: excerptTokens.length, + endIndex: excerptTokens.length + newTokens.length, + }, + isOptional: Boolean(parameter.optional), + isRest: parameter.name.startsWith('...'), + }); + excerptTokens.push(...newTokens); + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: `, ${jsDoc.params![index + 1]?.name}${jsDoc.params![index + 1]!.optional ? '?' : ''}: `, + }); + } + + if (jsDoc.params?.length) { + const parameter = jsDoc.params![jsDoc.params.length - 1]!; + const newTokens = this._mapVarType(parameter.type); + parameters.push({ + parameterName: parameter.name, + parameterTypeTokenRange: { + startIndex: excerptTokens.length, + endIndex: excerptTokens.length + newTokens.length, + }, + isOptional: Boolean(parameter.optional), + isRest: parameter.name.startsWith('...'), + }); + excerptTokens.push(...newTokens); + excerptTokens.push({ + kind: ExcerptTokenKind.Content, + text: `) => {})`, + }); + } + + const docComment: tsdoc.DocComment | undefined = new TSDocParser().parseString( + `/**\n * ${fixLinkTags(jsDoc.description)}\n${ + jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ?? '' + }${'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : ''}${ + 'deprecated' in jsDoc && jsDoc.deprecated + ? ` * @deprecated ${ + typeof jsDoc.deprecated === 'string' ? fixLinkTags(jsDoc.deprecated) : jsDoc.deprecated + }\n` + : '' + } */`, + ).docComment; + const releaseTag: ReleaseTag = ReleaseTag.Public; + + apiEvent = new ApiEvent({ + name, + docComment, + releaseTag, + excerptTokens, + overloadIndex: 0, + parameters, + fileUrlPath: filePathFromJson(jsDoc.meta), + fileLine: jsDoc.meta.line, + fileColumn: 0, + }); + parentApiItem.addMember(apiEvent); + } else { + // If the event was already declared before (via a merged interface declaration), + // we assume its signature is identical, because the language requires that. + } + } + /** * @param astDeclaration - The declaration * @param nodesToCapture - A list of child nodes whose token ranges we want to capture @@ -1131,4 +1556,43 @@ export class ApiModelGenerator { ); return sourceLocation; } + + private _mapVarType(typey: DocgenVarTypeJson): IExcerptToken[] { + const mapper = Array.isArray(typey) ? typey : typey.types ?? []; + const lookup: { [K in ts.SyntaxKind]?: string } = { + [ts.SyntaxKind.ClassDeclaration]: 'class', + [ts.SyntaxKind.InterfaceDeclaration]: 'interface', + [ts.SyntaxKind.TypeAliasDeclaration]: 'type', + }; + return mapper.flatMap((typ) => + typ.reduce( + (arr, [type, symbol]) => [ + ...arr, + { + kind: ExcerptTokenKind.Reference, + text: type ?? 'unknown', + canonicalReference: DeclarationReference.package(this._apiModel.packages[0]!.name) + .addNavigationStep(Navigation.Members as any, DeclarationReference.parseComponent(type ?? 'unknown')) + .withMeaning( + lookup[ + ( + (this._collector.entities.find( + (entity) => entity.nameForEmit === type && 'astDeclarations' in entity.astEntity, + )?.astEntity as AstSymbol | undefined) ?? + ( + this._collector.entities.find( + (entity) => entity.nameForEmit === type && 'astSymbol' in entity.astEntity, + )?.astEntity as AstImport | undefined + )?.astSymbol + )?.astDeclarations[0]?.declaration.kind ?? ts.SyntaxKind.ClassDeclaration + ] ?? ('class' as any), + ) + .toString(), + }, + { kind: ExcerptTokenKind.Content, text: symbol ?? '' }, + ], + [], + ), + ); + } } diff --git a/packages/api-extractor/src/generators/ExcerptBuilder.ts b/packages/api-extractor/src/generators/ExcerptBuilder.ts index 24798e1729df..a250d95e8262 100644 --- a/packages/api-extractor/src/generators/ExcerptBuilder.ts +++ b/packages/api-extractor/src/generators/ExcerptBuilder.ts @@ -1,7 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ExcerptTokenKind, type IExcerptToken, type IExcerptTokenRange } from '@discordjs/api-extractor-model'; +import { + ExcerptTokenKind, + type IExcerptToken, + type IExcerptTokenRange, + type IExcerptTokenRangeWithTypeParameters, +} from '@discordjs/api-extractor-model'; import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; import * as ts from 'typescript'; import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; @@ -126,6 +131,10 @@ export class ExcerptBuilder { return { startIndex: 0, endIndex: 0 }; } + public static createEmptyTokenRangeWithTypeParameters(): IExcerptTokenRangeWithTypeParameters { + return { startIndex: 0, endIndex: 0, typeParameters: [] }; + } + private static _buildSpan(excerptTokens: IExcerptToken[], span: Span, state: IBuildSpanState): boolean { if (span.kind === ts.SyntaxKind.JSDocComment) { // Discard any comments diff --git a/packages/api-extractor/src/index.ts b/packages/api-extractor/src/index.ts index 14a61e45716f..ecccc88c8898 100644 --- a/packages/api-extractor/src/index.ts +++ b/packages/api-extractor/src/index.ts @@ -3,7 +3,7 @@ /** * API Extractor helps with validation, documentation, and reviewing of the exported API for a TypeScript library. - * The `@microsoft/api-extractor` package provides the command-line tool. It also exposes a developer API that you + * The `@discordjs/api-extractor` package provides the command-line tool. It also exposes a developer API that you * can use to invoke API Extractor programmatically. * * @packageDocumentation diff --git a/packages/discord.js/api-extractor.json b/packages/discord.js/api-extractor.json new file mode 100644 index 000000000000..80ba4deedceb --- /dev/null +++ b/packages/discord.js/api-extractor.json @@ -0,0 +1,7 @@ +{ + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "/typings/index.d.ts", + "docModel": { + "projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/discord.js" + } +} diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 397158dcb13e..1c8407a46c2e 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -9,8 +9,9 @@ "lint": "prettier --check . && tslint typings/index.d.ts && cross-env ESLINT_USE_FLAT_CONFIG=false eslint --format=pretty src", "format": "prettier --write . && cross-env ESLINT_USE_FLAT_CONFIG=false eslint --fix --format=pretty src", "fmt": "pnpm run format", - "docs": "docgen -i './src/*.js' './src/**/*.js' -c ./docs/index.json -r ../../ -o ./docs/docs.json", + "docs": "docgen -i './src/*.js' './src/**/*.js' -c ./docs/index.json -r ../../ -o ./docs/docs.json && pnpm run docs:new", "docs:test": "docgen -i './src/*.js' './src/**/*.js' -c ./docs/index.json -r ../../", + "docs:new": "api-extractor -d run --local", "prepack": "pnpm run lint && pnpm run test", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/discord.js/*'", "release": "cliff-jumper" @@ -68,6 +69,7 @@ }, "devDependencies": { "@discordjs/docgen": "workspace:^", + "@discordjs/api-extractor": "workspace:^", "@favware/cliff-jumper": "2.2.1", "@types/node": "16.18.60", "cross-env": "^7.0.3", diff --git a/packages/discord.js/typings/tsdoc-metadata.json b/packages/discord.js/typings/tsdoc-metadata.json new file mode 100644 index 000000000000..216991b66607 --- /dev/null +++ b/packages/discord.js/typings/tsdoc-metadata.json @@ -0,0 +1,11 @@ +// This file is read by tools that parse documentation comments conforming to the TSDoc standard. +// It should be published with your NPM package. It should not be tracked by Git. +{ + "tsdocVersion": "0.12", + "toolPackages": [ + { + "packageName": "@discordjs/api-extractor", + "packageVersion": "7.38.1" + } + ] +} diff --git a/packages/docgen/bin/index.js b/packages/docgen/bin/index.js new file mode 100755 index 000000000000..c8dd7afae889 --- /dev/null +++ b/packages/docgen/bin/index.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../dist/bin/index.js'); diff --git a/packages/docgen/bin/index.ts b/packages/docgen/bin/index.ts index 5b6d049cd525..93a518ecb4f7 100644 --- a/packages/docgen/bin/index.ts +++ b/packages/docgen/bin/index.ts @@ -8,6 +8,7 @@ import { build } from '../src/index.js'; export interface CLIOptions { custom: string; input: string[]; + newOutput: string; output: string; root: string; typescript: boolean; diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 984711f3e0a3..a0942e8371eb 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -8,10 +8,9 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src", "fmt": "pnpm run format", - "prepare": "pnpm run build", "prepack": "pnpm run format && pnpm run build" }, - "bin": "./dist/bin/index.js", + "bin": "./bin/index.js", "exports": { ".": { "require": { diff --git a/packages/docgen/src/documentation.ts b/packages/docgen/src/documentation.ts index 2c27601fdb1b..6b56b4647ac1 100644 --- a/packages/docgen/src/documentation.ts +++ b/packages/docgen/src/documentation.ts @@ -268,5 +268,185 @@ export class Documentation { }; } + public serializeNew() { + return { + metadata: { + toolPackage: '@discordjs/docgen', + toolVersion: Documentation.FORMAT_VERSION, + schemaVersion: 1_011, + oldestForwardsCompatibleVersion: 1_001, + tsdocConfig: { + $schema: 'https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json', + noStandardTags: true, + tagDefinitions: [ + { + tagName: '@alpha', + syntaxKind: 'modifier', + }, + { + tagName: '@beta', + syntaxKind: 'modifier', + }, + { + tagName: '@defaultValue', + syntaxKind: 'block', + }, + { + tagName: '@decorator', + syntaxKind: 'block', + allowMultiple: true, + }, + { + tagName: '@deprecated', + syntaxKind: 'block', + }, + { + tagName: '@eventProperty', + syntaxKind: 'modifier', + }, + { + tagName: '@example', + syntaxKind: 'block', + allowMultiple: true, + }, + { + tagName: '@experimental', + syntaxKind: 'modifier', + }, + { + tagName: '@inheritDoc', + syntaxKind: 'inline', + }, + { + tagName: '@internal', + syntaxKind: 'modifier', + }, + { + tagName: '@label', + syntaxKind: 'inline', + }, + { + tagName: '@link', + syntaxKind: 'inline', + allowMultiple: true, + }, + { + tagName: '@override', + syntaxKind: 'modifier', + }, + { + tagName: '@packageDocumentation', + syntaxKind: 'modifier', + }, + { + tagName: '@param', + syntaxKind: 'block', + allowMultiple: true, + }, + { + tagName: '@privateRemarks', + syntaxKind: 'block', + }, + { + tagName: '@public', + syntaxKind: 'modifier', + }, + { + tagName: '@readonly', + syntaxKind: 'modifier', + }, + { + tagName: '@remarks', + syntaxKind: 'block', + }, + { + tagName: '@returns', + syntaxKind: 'block', + }, + { + tagName: '@sealed', + syntaxKind: 'modifier', + }, + { + tagName: '@see', + syntaxKind: 'block', + }, + { + tagName: '@throws', + syntaxKind: 'block', + allowMultiple: true, + }, + { + tagName: '@typeParam', + syntaxKind: 'block', + allowMultiple: true, + }, + { + tagName: '@virtual', + syntaxKind: 'modifier', + }, + { + tagName: '@betaDocumentation', + syntaxKind: 'modifier', + }, + { + tagName: '@internalRemarks', + syntaxKind: 'block', + }, + { + tagName: '@preapproved', + syntaxKind: 'modifier', + }, + ], + supportForTags: { + '@alpha': true, + '@beta': true, + '@defaultValue': true, + '@decorator': true, + '@deprecated': true, + '@eventProperty': true, + '@example': true, + '@experimental': true, + '@inheritDoc': true, + '@internal': true, + '@label': true, + '@link': true, + '@override': true, + '@packageDocumentation': true, + '@param': true, + '@privateRemarks': true, + '@public': true, + '@readonly': true, + '@remarks': true, + '@returns': true, + '@sealed': true, + '@see': true, + '@throws': true, + '@typeParam': true, + '@virtual': true, + '@betaDocumentation': true, + '@internalRemarks': true, + '@preapproved': true, + }, + reportUnsupportedHtmlElements: false, + }, + }, + projectFolderUrl: 'https://github.com/discordjs/discord.js/tree/main/packages/discord.js', + kind: 'Package', + canonicalReference: 'discord.js!', + docComment: '', + name: 'discord.js', + preserveMemberOrder: false, + members: [ + ...[...this.classes.values()].map((_class) => _class.serialize()), + ...[...this.functions.values()].map((_function) => _function.serialize()), + ...[...this.interfaces.values()].map((_interface) => _interface.serialize()), + ...[...this.typedefs.values()].map((_typedef) => _typedef.serialize()), + ...[...this.externals.values()].map((_external) => _external.serialize()), + ], + custom: this.custom, + }; + } + public static readonly FORMAT_VERSION = 30; } diff --git a/packages/docgen/src/index.ts b/packages/docgen/src/index.ts index 9d24676d6d0d..0140ec039399 100644 --- a/packages/docgen/src/index.ts +++ b/packages/docgen/src/index.ts @@ -17,7 +17,7 @@ interface CustomFiles { path?: string; } -export async function build({ input, custom: customDocs, root, output, typescript }: CLIOptions) { +export async function build({ input, custom: customDocs, root, output, newOutput, typescript }: CLIOptions) { let data: (ChildTypes & RootTypes)[] | DeclarationReflection[] = []; if (typescript) { console.log('Parsing Typescript in source files...'); @@ -82,5 +82,10 @@ export async function build({ input, custom: customDocs, root, output, typescrip writeFileSync(output, JSON.stringify(docs.serialize())); } + if (newOutput) { + console.log(`Writing to ${newOutput}...`); + writeFileSync(newOutput, JSON.stringify(docs.serializeNew())); + } + console.log('Done!'); } diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 48b0e57cdf6e..23cceeddb599 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -5,7 +5,7 @@ "description": "A set of scripts that we use for our workflows", "private": true, "scripts": { - "build": "tsc --noEmit && tsup", + "build": "tsc --noEmit --skipLibCheck && tsup", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src turbo", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src turbo", "fmt": "pnpm run format" @@ -53,10 +53,12 @@ }, "homepage": "https://discord.js.org", "dependencies": { - "@discordjs/api-extractor-utils": "workspace:^", + "@actions/glob": "^0.4.0", "@discordjs/api-extractor-model": "workspace:^", + "@discordjs/api-extractor-utils": "workspace:^", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "0.16.2", + "@planetscale/database": "^1.11.0", "tslib": "^2.6.2", "undici": "5.27.2", "yaml": "2.3.4" @@ -66,6 +68,7 @@ "@types/node": "18.18.8", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", + "env-cmd": "^10.1.0", "eslint": "^8.53.0", "eslint-config-neon": "^0.1.57", "eslint-formatter-pretty": "^5.0.0", diff --git a/packages/scripts/src/generateIndex.ts b/packages/scripts/src/generateIndex.ts index ee0c0f576d7a..8fbe3b858b25 100644 --- a/packages/scripts/src/generateIndex.ts +++ b/packages/scripts/src/generateIndex.ts @@ -1,23 +1,11 @@ import { stat, mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { cwd } from 'node:process'; -import { - ApiModel, - ApiDeclaredItem, - ApiItemContainerMixin, - ApiItem, - type ApiPackage, - ApiItemKind, -} from '@discordjs/api-extractor-model'; +import type { ApiPackage } from '@discordjs/api-extractor-model'; +import { ApiItem, ApiModel, ApiDeclaredItem, ApiItemContainerMixin, ApiItemKind } from '@discordjs/api-extractor-model'; import { generatePath } from '@discordjs/api-extractor-utils'; -import { - DocNodeKind, - type DocCodeSpan, - type DocNode, - type DocParagraph, - type DocPlainText, - TSDocConfiguration, -} from '@microsoft/tsdoc'; +import { DocNodeKind, TSDocConfiguration } from '@microsoft/tsdoc'; +import type { DocLinkTag, DocCodeSpan, DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; import { request } from 'undici'; @@ -29,6 +17,7 @@ export interface MemberJSON { } export const PACKAGES = [ + 'discord.js', 'brokers', 'builders', 'collection', @@ -44,17 +33,23 @@ export const PACKAGES = [ let idx = 0; export function addPackageToModel(model: ApiModel, data: any) { - const tsdocConfiguration = new TSDocConfiguration(); - const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig); - tsdocConfigFile.configureParser(tsdocConfiguration); - - const apiPackage = ApiItem.deserialize(data, { - apiJsonFilename: '', - toolPackage: data.metadata.toolPackage, - toolVersion: data.metadata.toolVersion, - versionToDeserialize: data.metadata.schemaVersion, - tsdocConfiguration, - }) as ApiPackage; + let apiPackage: ApiPackage; + if (data.metadata) { + const tsdocConfiguration = new TSDocConfiguration(); + const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig); + tsdocConfigFile.configureParser(tsdocConfiguration); + + apiPackage = ApiItem.deserialize(data, { + apiJsonFilename: '', + toolPackage: data.metadata.toolPackage, + toolVersion: data.metadata.toolVersion, + versionToDeserialize: data.metadata.schemaVersion, + tsdocConfiguration, + }) as ApiPackage; + } else { + apiPackage = ApiItem.deserializeDocgen(data, 'discord.js') as ApiPackage; + } + model.addMember(apiPackage); return model; } @@ -82,6 +77,9 @@ export function tryResolveSummaryText(item: ApiDeclaredItem): string | null { case DocNodeKind.PlainText: retVal += (node as DocPlainText).text; break; + case DocNodeKind.LinkTag: + retVal += (node as DocLinkTag).urlDestination; + break; case DocNodeKind.Section: case DocNodeKind.Paragraph: { for (const child of (node as DocParagraph).nodes) { diff --git a/packages/scripts/src/populateDevDatabaseBranch.ts b/packages/scripts/src/populateDevDatabaseBranch.ts new file mode 100644 index 000000000000..de48b654e443 --- /dev/null +++ b/packages/scripts/src/populateDevDatabaseBranch.ts @@ -0,0 +1,32 @@ +import { readFile } from 'node:fs/promises'; +import process, { cwd } from 'node:process'; +import { create } from '@actions/glob'; +import { connect } from '@planetscale/database'; + +const sql = connect({ + url: process.env.DATABASE_URL!, +}); + +process.chdir(`${cwd()}/../../`); +const globber = await create(`packages/*/docs/*.api.json`); +// const globber2 = await create(`discord.js/*.json`); +for await (const file of globber.globGenerator()) { + const parsed = /(?\d+.\d+.\d+)-?.*/.exec(file); + const data = await readFile(file, 'utf8'); + + if (parsed?.groups) { + console.log(parsed.groups.semver, file); + try { + await sql.execute('replace into documentation (version, data) values (?, ?)', [parsed.groups.semver, data]); + } catch (error) { + console.error(error); + } + } else { + console.log('main', file); + try { + await sql.execute('replace into documentation (version, data) values (?, ?)', ['main', data]); + } catch (error) { + console.error(error); + } + } +} diff --git a/packages/scripts/tsup.config.ts b/packages/scripts/tsup.config.ts index 6ed89fd764af..034e2ebc5b5e 100644 --- a/packages/scripts/tsup.config.ts +++ b/packages/scripts/tsup.config.ts @@ -1,5 +1,12 @@ import { createTsupConfig } from '../../tsup.config.js'; -export default createTsupConfig({ - minify: 'terser', -}); +export default [ + createTsupConfig({ + minify: 'terser', + }), + createTsupConfig({ + entry: ['src/populateDevDatabaseBranch.ts'], + format: 'esm', + minify: 'terser', + }), +]; diff --git a/patches/next@14.0.2-canary.20.patch b/patches/next@14.0.2-canary.20.patch new file mode 100644 index 000000000000..56d36a0bfc40 --- /dev/null +++ b/patches/next@14.0.2-canary.20.patch @@ -0,0 +1,36 @@ +diff --git a/dist/esm/server/lib/incremental-cache/index.js b/dist/esm/server/lib/incremental-cache/index.js +index 93519fc1e45361f153edd1f4496ca4a006745e8f..8d603be638c297db9593ae9fa92ec5359d38f823 100644 +--- a/dist/esm/server/lib/incremental-cache/index.js ++++ b/dist/esm/server/lib/incremental-cache/index.js +@@ -348,10 +348,11 @@ export class IncrementalCache { + }); + } + if (this.dev && !ctx.fetchCache) return; ++ const max_fetch_size = Number(process.env.MAX_FETCH_SIZE) || 5 // gets value set in next.config + // fetchCache has upper limit of 2MB per-entry currently +- if (ctx.fetchCache && JSON.stringify(data).length > 2 * 1024 * 1024) { ++ if (ctx.fetchCache && JSON.stringify(data).length > max_fetch_size * 1024 * 1024) { + if (this.dev) { +- throw new Error(`fetch for over 2MB of data can not be cached`); ++ throw new Error(`fetch for over ${max_fetch_size}MB of data can not be cached`); + } + return; + } +diff --git a/dist/server/lib/incremental-cache/index.js b/dist/server/lib/incremental-cache/index.js +index 83e061e5a072d253c2db7ea5e796307070cb1f81..5f9a462e3e12965a49ab6419b19ef2fa0aae7fe6 100644 +--- a/dist/server/lib/incremental-cache/index.js ++++ b/dist/server/lib/incremental-cache/index.js +@@ -375,10 +375,11 @@ class IncrementalCache { + }); + } + if (this.dev && !ctx.fetchCache) return; ++ const max_fetch_size = Number(process.env.MAX_FETCH_SIZE) || 5 //gets value set in next.config + // fetchCache has upper limit of 2MB per-entry currently +- if (ctx.fetchCache && JSON.stringify(data).length > 2 * 1024 * 1024) { ++ if (ctx.fetchCache && JSON.stringify(data).length > max_fetch_size * 1024 * 1024) { + if (this.dev) { +- throw new Error(`fetch for over 2MB of data can not be cached`); ++ throw new Error(`fetch for over ${max_fetch_size}MB of data can not be cached`); + } + return; + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd6189bad784..5743d0b99e44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ patchedDependencies: '@microsoft/tsdoc-config@0.16.2': hash: 35av6rrndvjtr2u2jso66jatbu path: patches/@microsoft__tsdoc-config@0.16.2.patch + next@14.0.2-canary.20: + hash: 3u4mq7bxekreeqimcfnpxiw27y + path: patches/next@14.0.2-canary.20.patch importers: @@ -112,14 +115,14 @@ importers: specifier: ^0.3.4 version: 0.3.4 next: - specifier: ^14.0.2-canary.14 - version: 14.0.2-canary.14(react-dom@18.2.0)(react@18.2.0) + specifier: 14.0.2-canary.20 + version: 14.0.2-canary.20(patch_hash=3u4mq7bxekreeqimcfnpxiw27y)(react-dom@18.2.0)(react@18.2.0) next-contentlayer: specifier: ^0.3.4 - version: 0.3.4(contentlayer@0.3.4)(next@14.0.2-canary.14)(react-dom@18.2.0)(react@18.2.0) + version: 0.3.4(contentlayer@0.3.4)(next@14.0.2-canary.20)(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.0.2-canary.14)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.0.2-canary.20)(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -143,8 +146,8 @@ importers: version: 0.32.6 devDependencies: '@next/bundle-analyzer': - specifier: ^14.0.1 - version: 14.0.1 + specifier: 14.0.2-canary.20 + version: 14.0.2-canary.20 '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) @@ -278,14 +281,14 @@ importers: specifier: ^0.35.0 version: 0.35.0 next: - specifier: ^14.0.2-canary.14 - version: 14.0.2-canary.14(react-dom@18.2.0)(react@18.2.0) + specifier: 14.0.2-canary.20 + version: 14.0.2-canary.20(patch_hash=3u4mq7bxekreeqimcfnpxiw27y)(react-dom@18.2.0)(react@18.2.0) next-mdx-remote: specifier: ^4.4.1 version: 4.4.1(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.0.2-canary.14)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.0.2-canary.20)(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -309,8 +312,8 @@ importers: version: 0.32.6 devDependencies: '@next/bundle-analyzer': - specifier: ^14.0.1 - version: 14.0.1 + specifier: 14.0.2-canary.20 + version: 14.0.2-canary.20 '@testing-library/react': specifier: ^14.0.0 version: 14.0.0(react-dom@18.2.0)(react@18.2.0) @@ -941,6 +944,9 @@ importers: specifier: 8.14.2 version: 8.14.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@discordjs/docgen': specifier: workspace:^ version: link:../docgen @@ -1322,6 +1328,9 @@ importers: packages/scripts: dependencies: + '@actions/glob': + specifier: ^0.4.0 + version: 0.4.0 '@discordjs/api-extractor-model': specifier: workspace:^ version: link:../api-extractor-model @@ -1334,6 +1343,9 @@ importers: '@microsoft/tsdoc-config': specifier: 0.16.2 version: 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) + '@planetscale/database': + specifier: ^1.11.0 + version: 1.11.0 tslib: specifier: ^2.6.2 version: 2.6.2 @@ -1356,6 +1368,9 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + env-cmd: + specifier: ^10.1.0 + version: 10.1.0 eslint: specifier: ^8.53.0 version: 8.53.0 @@ -4784,8 +4799,8 @@ packages: tar-fs: 2.1.1 dev: true - /@next/bundle-analyzer@14.0.1: - resolution: {integrity: sha512-AbZZnj4gZ1ZQFppZxAC9e8+skj0rFiSvY6E6Ut+ydS1r6oizR7PMu/7o02psIm4ekAsmp2O1Eq8IowHQgPWPCQ==} + /@next/bundle-analyzer@14.0.2-canary.20: + resolution: {integrity: sha512-8hqQkCSVI2QJXEcCIZj+xbSHP5Gv9vSAkm66CY8x09SfrYAUPGkSj+KrqbjzQ++A6sSFG2/E1INudWdTl80eRA==} dependencies: webpack-bundle-analyzer: 4.7.0 transitivePeerDependencies: @@ -4793,8 +4808,8 @@ packages: - utf-8-validate dev: true - /@next/env@14.0.2-canary.14: - resolution: {integrity: sha512-XBCanDyEL36mcVlmnjLXjEEHiQmuZf0L5ddNbJpjAPcsQ6fp/IMZszVk4380DAFWuGr0+ozH8cGpheGlHHlmhQ==} + /@next/env@14.0.2-canary.20: + resolution: {integrity: sha512-WToL6j4EPN3Upfo4X4XGfBCKPA+5e3Lsq56imOfq33hag1iBa0OBTfU2eRVJE2W7YIECSm0hFzQiZF5OaYTgdg==} dev: false /@next/eslint-plugin-next@13.5.6: @@ -4803,8 +4818,8 @@ packages: glob: 7.1.7 dev: true - /@next/swc-darwin-arm64@14.0.2-canary.14: - resolution: {integrity: sha512-7OYCeqXTAiaODsP5ENfPsu3NGpi9NrbFZnG7Y7CrsCsMpnfJ62mPR3XTRuMVS7oDcsbky+9J7zXxSo49OCJkEw==} + /@next/swc-darwin-arm64@14.0.2-canary.20: + resolution: {integrity: sha512-Vf1rrBRd2iTsoOa0icTr7nMVpex8iA+looWnBGPm5K+9yCvD23alETQ+pjAEGUPec98wPj2mTUfZzBU8rsZANw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -4812,8 +4827,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.0.2-canary.14: - resolution: {integrity: sha512-ZYnV2riRc6+ojxyLsIO0h6mZwrlBnY+cUbisvCnnt5NiFnMXRNNQYLDQ+d4/T4mOMPQvLUfIqKAPaOgT4OgWLw==} + /@next/swc-darwin-x64@14.0.2-canary.20: + resolution: {integrity: sha512-fuN29g3jd9g8De3MIFEpVzhWQ9BUwTorDuIrW5rbsD6lZCasKDVk4eYJ8zeOf7nQrqjRCVOb9a8JteYJsGj/nQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -4821,8 +4836,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.0.2-canary.14: - resolution: {integrity: sha512-m5r8JqtWnrTqkalJV1N/mkfuTwFZojPxbnlNtivilj4aiqMq81obgQ2i6a3dXvIF5UZIwkR33aMDbTZI6swaEA==} + /@next/swc-linux-arm64-gnu@14.0.2-canary.20: + resolution: {integrity: sha512-M+JsTvZqgW0DVPqk0IAShWWrvshCruuoabVeBpMeRxTGSfzgYzf3SP9GB2XEd8gfEWJn+P+xB7zXbT8WaNxGTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -4830,8 +4845,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.0.2-canary.14: - resolution: {integrity: sha512-VWQ6jz3RMARElYcIXC7TxAwmS6MCYowgXUthdDJtwaeUfgSp14S4fg3e2NHsveI1VPvUABBJ16DIpLHUxsk9sw==} + /@next/swc-linux-arm64-musl@14.0.2-canary.20: + resolution: {integrity: sha512-dgQ0i7arRBKVzsWV6ioOUWxaCIho8MKLeSsgXPBCgbBtX4TscJrrfcLh9lFmvrlMj7CwM+eAhrVNrRcOb5mncw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -4839,8 +4854,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.0.2-canary.14: - resolution: {integrity: sha512-pxvwxNMSr9MKlSMzgj544oCyHe4iwZz3XVZnBaYIS4V0hcQDL5NOPVnYYkaYomlwy8fg26UOTkhfIaxeQN6Q1w==} + /@next/swc-linux-x64-gnu@14.0.2-canary.20: + resolution: {integrity: sha512-6t7sQaCgjdnocMoP1TzGi2PDf1qPtVQpDMbqx52V38kR30dlc1S3EvwAmReSzuWlfqwRpYtwJ79O9F7KzvCbDA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -4848,8 +4863,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.0.2-canary.14: - resolution: {integrity: sha512-j+tyC9byzdX0AC92TZT2wDRiKzPYpyUk0EPsb6vVUY4nsAH5oJfASof9hWWpQkAf1REa6PJFJUAnQmDyAYkBNw==} + /@next/swc-linux-x64-musl@14.0.2-canary.20: + resolution: {integrity: sha512-Ub/3n3ZkjdpcnD1ZdCblS6s7zplT1TTO45RmZKWKpJRKv/5vttrZ8P4xuaklvJSaLlHVEzv8wxU/xuAPHM/q0A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -4857,8 +4872,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.0.2-canary.14: - resolution: {integrity: sha512-m/SXCsFvJxzgVFhHyJTIzqOtQwpFCVZvO/LphGVRz+8sVGKJYn/xtV87Q/g3mvXzsfRE3vs6VfDUubz+ZNlpSQ==} + /@next/swc-win32-arm64-msvc@14.0.2-canary.20: + resolution: {integrity: sha512-4C1r+h8qfWIYy7350ExcjWFCwx4sKASE0IwkGpZez8m6MPnKpCyz3Tr/1QJZ15N8ZVPB0IHf+LcPDZR5xyRO4A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -4866,8 +4881,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.0.2-canary.14: - resolution: {integrity: sha512-r9v0J88eOd3vhlSrn7V7rfu4gSWaqAyZuXyoE0MTFAOsfeOYwQwJ3vKbr82/560zLQ1rgfaUuA68JPrSNyHL1g==} + /@next/swc-win32-ia32-msvc@14.0.2-canary.20: + resolution: {integrity: sha512-GaaGR4O2dH0/c1gZ4c/3rgaSYvioo8V1mjdGrJuSYohPW+UL11yHMBqfBxOfNNlaRDV/qZHcT6J4A/MHmy7liQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -4875,8 +4890,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.0.2-canary.14: - resolution: {integrity: sha512-l3OnTrRhXZS4vcDA0BWcq9u8AtBA6nHDbuZjW0GCiUlzl/B6MIegUEsXw5uXU32O0VBArI3xB4BCuQANQBSzrw==} + /@next/swc-win32-x64-msvc@14.0.2-canary.20: + resolution: {integrity: sha512-R36N7/cdW/dk/0w5T7wmH90HdzfpErn9xuQ73oEz2GX7yc1iUhSBMiM6HjzomZWfXbQ57W0d0BmQYFmbVAeWug==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -11686,6 +11701,15 @@ packages: engines: {node: '>=0.12'} dev: true + /env-cmd@10.1.0: + resolution: {integrity: sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + commander: 4.1.1 + cross-spawn: 7.0.3 + dev: true + /envinfo@7.11.0: resolution: {integrity: sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==} engines: {node: '>=4'} @@ -17389,7 +17413,7 @@ packages: engines: {node: '>= 0.4.0'} dev: true - /next-contentlayer@0.3.4(contentlayer@0.3.4)(next@14.0.2-canary.14)(react-dom@18.2.0)(react@18.2.0): + /next-contentlayer@0.3.4(contentlayer@0.3.4)(next@14.0.2-canary.20)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UtUCwgAl159KwfhNaOwyiI7Lg6sdioyKMeh+E7jxx0CJ29JuXGxBEYmCI6+72NxFGIFZKx8lvttbbQhbnYWYSw==} peerDependencies: contentlayer: 0.3.4 @@ -17409,7 +17433,7 @@ packages: '@contentlayer/core': 0.3.4 '@contentlayer/utils': 0.3.4 contentlayer: 0.3.4 - next: 14.0.2-canary.14(react-dom@18.2.0)(react@18.2.0) + next: 14.0.2-canary.20(patch_hash=3u4mq7bxekreeqimcfnpxiw27y)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -17440,7 +17464,7 @@ packages: - supports-color dev: false - /next-themes@0.2.1(next@14.0.2-canary.14)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.0.2-canary.20)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' @@ -17454,13 +17478,13 @@ packages: react-dom: optional: true dependencies: - next: 14.0.2-canary.14(react-dom@18.2.0)(react@18.2.0) + next: 14.0.2-canary.20(patch_hash=3u4mq7bxekreeqimcfnpxiw27y)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.0.2-canary.14(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+7w8b5o08OQoGBglTXpruuYqyomhwMsBlHPwDsq9DeHFtsDeXOLrrll5vGITH/4RYTsPxwvQ9nngN1EIXGWKYA==} + /next@14.0.2-canary.20(patch_hash=3u4mq7bxekreeqimcfnpxiw27y)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-8fRym/rsqDEGxy0LNDYeVdvzfIBh4mzfIBTtN5lPraftZlknobkyVXOwkQKleG9pu3ml8AloJDj/lKiaLWvzBQ==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -17478,7 +17502,7 @@ packages: sass: optional: true dependencies: - '@next/env': 14.0.2-canary.14 + '@next/env': 14.0.2-canary.20 '@swc/helpers': 0.5.2 busboy: 1.6.0 caniuse-lite: 1.0.30001561 @@ -17488,19 +17512,20 @@ packages: styled-jsx: 5.1.1(react@18.2.0) watchpack: 2.4.0 optionalDependencies: - '@next/swc-darwin-arm64': 14.0.2-canary.14 - '@next/swc-darwin-x64': 14.0.2-canary.14 - '@next/swc-linux-arm64-gnu': 14.0.2-canary.14 - '@next/swc-linux-arm64-musl': 14.0.2-canary.14 - '@next/swc-linux-x64-gnu': 14.0.2-canary.14 - '@next/swc-linux-x64-musl': 14.0.2-canary.14 - '@next/swc-win32-arm64-msvc': 14.0.2-canary.14 - '@next/swc-win32-ia32-msvc': 14.0.2-canary.14 - '@next/swc-win32-x64-msvc': 14.0.2-canary.14 + '@next/swc-darwin-arm64': 14.0.2-canary.20 + '@next/swc-darwin-x64': 14.0.2-canary.20 + '@next/swc-linux-arm64-gnu': 14.0.2-canary.20 + '@next/swc-linux-arm64-musl': 14.0.2-canary.20 + '@next/swc-linux-x64-gnu': 14.0.2-canary.20 + '@next/swc-linux-x64-musl': 14.0.2-canary.20 + '@next/swc-win32-arm64-msvc': 14.0.2-canary.20 + '@next/swc-win32-ia32-msvc': 14.0.2-canary.20 + '@next/swc-win32-x64-msvc': 14.0.2-canary.20 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros dev: false + patched: true /no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}