Compare commits

...

42 Commits

Author SHA1 Message Date
Vlad Frangu
ae58053dc8 chore: release @discordjs/rest@2.2.0 & @discordjs/core@1.1.1 (#9978)
* chore(rest): release @discordjs/rest@2.2.0

* chore(core): release @discordjs/core@1.1.1

* chore: at this point might as well hand write
2023-11-18 00:01:40 +00:00
Jiralite
5da5be2bc9 fix(route): Conditionally prefix package names (#9975)
fix(route): conditionally prefix package names
2023-11-17 23:38:49 +01:00
Noel
55be1c901a ci: properly extract name from minified docs files 2023-11-17 23:37:14 +01:00
Qjuh
492f86af39 refactor: minify api.json by shortening keys (#9971)
* refactor: minify api.json by shortening keys

* fix: links to other packages

* refactor: get doclink from canonicalReference, not model

* fix: types

* fix: again

* fix: @link tags with alt texts
2023-11-17 23:26:48 +01:00
Qjuh
9868772b64 docs: fix links in @deprecated tags (#9976)
types: fix links in @deprecated tags
2023-11-17 20:44:39 +00:00
Vlad Frangu
6df233de14 feat: present x-ratelimit-scope for 429s hit (#9973)
* feat: present x-ratelimit-scope for 429s hit

* fix: get scope from headers for burst too
2023-11-16 10:49:05 +00:00
Qjuh
0aa7dc1b86 fix: replace Object< with Record< (#9970) 2023-11-15 20:20:21 +01:00
Qjuh
cab60142ff fix: type mapping for docgen methods/props (#9969)
* fix: minify mainlib docs json

* fix: minify them all

* fix: type mapping for docgen methods/props
2023-11-14 20:54:29 +00:00
Qjuh
4b88306dcb fix: minify mainlib docs json (#9963)
* fix: minify mainlib docs json

* fix: minify them all
2023-11-14 09:13:28 +01:00
iCrawl
f9177be61b fix: don't hardcode discordjs special case 2023-11-14 02:04:02 +01:00
iCrawl
75137bac6f refactor: don't prebuild on preview builds 2023-11-14 01:55:08 +01:00
iCrawl
00063912ee fix: conflict resolution 2023-11-14 01:35:08 +01:00
iCrawl
8f432400d8 build: multi-config build and dep update 2023-11-14 01:26:22 +01:00
iCrawl
75fc7f2454 ci: include secret 2023-11-13 23:21:35 +01:00
iCrawl
01c63d2e0f refactor: switch to vercel blob for docs 2023-11-13 23:15:16 +01:00
iCrawl
ffc3ea5c3f fix: hardcode redirect for main lib for now 2023-11-13 21:37:59 +01:00
iCrawl
1d2d01e1f5 ci: dont set the ci to failed on fail to upload docs 2023-11-13 21:31:46 +01:00
iCrawl
0063dae43b fix: use correct pkg variable 2023-11-13 21:30:05 +01:00
iCrawl
69c949ab28 fix: properly switch versions 2023-11-13 21:17:42 +01:00
iCrawl
25d552b318 fix: missing on conflict clause 2023-11-13 21:09:43 +01:00
iCrawl
c4767bacde refactor: switch to vercel pg 2023-11-13 21:07:01 +01:00
iCrawl
9a8110047e fix: cache nextjs cache output with turbo 2023-11-13 19:55:08 +01:00
iCrawl
d4ebc369ca fix: dont cache nextjs cache 2023-11-13 19:31:55 +01:00
iCrawl
b150d4ac27 fix: provide full path for pre-gen 2023-11-13 19:19:26 +01:00
iCrawl
81a892e27f fix: properly create index 2023-11-13 19:13:32 +01:00
iCrawl
bc8f83368a feat: reintroduce outline navigation 2023-11-13 18:03:23 +01:00
Qjuh
7c935dc84b feat: docs for mixin methods, examples (#9960) 2023-11-13 15:03:47 +01:00
iCrawl
8d04cbc203 chore: add loading page for suspense 2023-11-13 11:23:59 +01:00
iCrawl
356cadb382 chore: enable prefetch 2023-11-13 11:07:47 +01:00
iCrawl
978a39f6d3 chore: disable prefetch 2023-11-13 10:48:51 +01:00
iCrawl
516be87a87 fix: properly display all versions in selector 2023-11-13 10:22:44 +01:00
Qjuh
b79351ba99 fix(website): misc improvements (#9940)
* refactor: use tokenRange for typeParams in heritage

* fix: correct type param replacement

* fix: ae config, link builtin in summary, `: | T` => `: T`, mainlib tsdoc

* fix: requested changes and tests

* chore: better deprecation messages and code cleanup

* fix: cleanup optional chainings

---------

Co-authored-by: Almeida <almeidx@pm.me>
2023-11-13 09:55:23 +01:00
iCrawl
2d63d93558 ci: add readmes to build output for cache 2023-11-13 00:17:43 +01:00
iCrawl
b305194841 ci: revert deploying vercel with ci for now 2023-11-13 00:12:37 +01:00
Vlad Frangu
2550c7931d chore(discord.js): release discord.js@14.14.1 (#9957)
* chore(discord.js): release discord.js@14.14.1

* chore: actual changelog
2023-11-12 22:06:14 +00:00
Almeida
40726db722 refactor: use formatters (#9956)
* refactor: use formatters

* fix: imports

* fix: imports pt.2

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-11-12 21:56:10 +00:00
Aura
1e4ef35436 docs: use preferred nullable syntax (?T over T | null) (#9946)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-11-12 21:45:02 +00:00
Aura
4bc1dae36f types: use wrapper utilities (#9945)
* types: use `Awaitable<T>` instead of `Promise<T> | T`

* types: use `JSONEncodable<T>` over raw definition

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-11-12 21:40:59 +00:00
Almeida
cc07a28f12 fix(Emoji): id set as undefined edge case (#9953) 2023-11-12 21:34:24 +00:00
Jiralite
f93abf7e35 fix(BaseClient): Default in objects properly (#9952)
fix(BaseClient): default in objects properly
2023-11-12 21:14:09 +00:00
Jiralite
f0ec70dfda feat: bump package versions (#9951)
* feat: bump package versions

* chore(create-discord-bot): release create-discord-bot@0.2.3

---------

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
2023-11-12 20:22:55 +00:00
Jaw0r3k
8a6045f600 fix: import picocolors as default (#9949) 2023-11-12 19:39:08 +00:00
204 changed files with 2250 additions and 1101 deletions

View File

@@ -8,21 +8,19 @@ jobs:
deploy-website:
name: Deploy website
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build & deploy website
uses: BetaHuhn/deploy-to-vercel-action@643bc80032ba62ca41d1a9aaba7b38b51c2b8646
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_SCOPE: 'discordjs'
GITHUB_DEPLOYMENT_ENV: 'Production discord-js'
PRODUCTION: true
- name: Pull vercel production environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build website artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy website artifacts to vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

View File

@@ -71,6 +71,7 @@ jobs:
if: ${{ github.ref_type == 'tag' }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./packages/actions/src/uploadDocumentation
with:
package: ${{ steps.extract-tag.outputs.package }}
@@ -94,6 +95,7 @@ jobs:
if: ${{ github.ref_type == 'branch' }}
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
uses: ./packages/actions/src/uploadDocumentation
- name: Move docs to correct directory
@@ -149,26 +151,3 @@ jobs:
SEARCH_API_URL: ${{ secrets.SEARCH_API_URL }}
SEARCH_API_KEY: ${{ secrets.SEARCH_API_KEY }}
uses: ./packages/actions/src/uploadSearchIndices
deploy-website:
needs: build-docs
name: Deploy website
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
if: github.repository_owner == 'discordjs'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build & deploy website
uses: BetaHuhn/deploy-to-vercel-action@643bc80032ba62ca41d1a9aaba7b38b51c2b8646
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_SCOPE: 'discordjs'
GITHUB_DEPLOYMENT_ENV: 'Production discord-js'
PRODUCTION: true

View File

@@ -89,48 +89,50 @@
* DEFAULT VALUE: no overrideTsconfig section
*/
"overrideTsconfig": {
// Type Checking
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"compilerOptions": {
// Type Checking
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
// Modules
"allowArbitraryExtensions": false,
"allowImportingTsExtensions": false,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"resolvePackageJsonExports": false,
"resolvePackageJsonImports": false,
// Modules
"allowArbitraryExtensions": false,
"allowImportingTsExtensions": false,
"module": "ESNext",
"moduleResolution": "nodenext",
"resolveJsonModule": true,
"resolvePackageJsonExports": false,
"resolvePackageJsonImports": false,
// Emit
"declaration": true,
"declarationMap": true,
"importHelpers": false,
"newLine": "lf",
"noEmitHelpers": true,
"outDir": "dist",
"removeComments": false,
"sourceMap": true,
// Emit
"declaration": true,
"declarationMap": true,
"importHelpers": false,
"newLine": "lf",
"noEmitHelpers": true,
"outDir": "dist",
"removeComments": false,
"sourceMap": true,
// Interop Constraints
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
// Interop Constraints
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
// Language and Environment
"experimentalDecorators": true,
"lib": ["ESNext"],
"target": "ES2022",
"useDefineForClassFields": true
// Language and Environment
"experimentalDecorators": true,
"lib": ["ESNext"],
"target": "ES2022",
"useDefineForClassFields": true
}
}
/**
* This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -53,7 +53,7 @@
"ariakit": "2.0.0-next.44",
"cmdk": "^0.2.0",
"contentlayer": "^0.3.4",
"next": "14.0.3-canary.2",
"next": "14.0.3-canary.5",
"next-contentlayer": "^0.3.4",
"next-themes": "^0.2.1",
"react": "^18.2.0",
@@ -65,7 +65,7 @@
"sharp": "^0.32.6"
},
"devDependencies": {
"@next/bundle-analyzer": "14.0.3-canary.2",
"@next/bundle-analyzer": "14.0.3-canary.5",
"@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1",
"@types/html-escaper": "^3.0.2",
@@ -86,7 +86,7 @@
"hastscript": "^8.0.0",
"html-escaper": "^3.0.3",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",
"unocss": "^0.57.3",

View File

@@ -23,7 +23,9 @@ export function Nav() {
)}
universal
>
<Sidebar />
<div className="flex flex-col gap-4 p-3">
<Sidebar />
</div>
</Scrollbars>
</nav>
);

View File

@@ -32,7 +32,7 @@ export function Sidebar() {
const { setOpened } = useNav();
return (
<div className="flex flex-col gap-3 p-3">
<div className="flex flex-col gap-4">
{Object.keys(itemsByCategory).map((category, idx) => (
<Section
buttonClassName="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-400 dark:hover:bg-dark-300 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-none focus:ring z-10"
@@ -41,7 +41,7 @@ export function Sidebar() {
>
{itemsByCategory[category]?.map((member, index) => (
<Link
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l first:mt-1 p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
decodeURIComponent(pathname ?? '') === member.href
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'

View File

@@ -0,0 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +0,0 @@
export * from '../../.lintstagedrc.json' assert { type: 'json' };

View File

@@ -0,0 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -1 +0,0 @@
export * from '../../.prettierrc.json' assert { type: 'json' };

View File

@@ -52,17 +52,17 @@
"@discordjs/ui": "workspace:^",
"@microsoft/tsdoc": "^0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@planetscale/database": "^1.11.0",
"@react-icons/all-files": "^4.1.0",
"@vercel/analytics": "^1.1.1",
"@vercel/edge-config": "^0.4.1",
"@vercel/og": "^0.5.20",
"@vercel/postgres": "^0.5.1",
"ariakit": "2.0.0-next.44",
"bright": "^0.8.4",
"class-variance-authority": "^0.7.0",
"cmdk": "^0.2.0",
"meilisearch": "^0.35.0",
"next": "14.0.3-canary.2",
"next": "14.0.3-canary.5",
"next-mdx-remote": "^4.4.1",
"next-themes": "^0.2.1",
"react": "^18.2.0",
@@ -74,7 +74,7 @@
"sharp": "^0.32.6"
},
"devDependencies": {
"@next/bundle-analyzer": "14.0.3-canary.2",
"@next/bundle-analyzer": "14.0.3-canary.5",
"@testing-library/react": "^14.1.0",
"@testing-library/user-event": "^14.5.1",
"@types/node": "18.18.8",
@@ -92,7 +92,7 @@
"eslint-formatter-pretty": "^5.0.0",
"happy-dom": "^12.10.3",
"postcss": "^8.4.31",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",
"vercel": "^32.5.3",

View File

@@ -3,13 +3,9 @@ import { generateAllIndices } from '@discordjs/scripts';
console.log('Generating all indices...');
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'));
return JSON.parse(await readFile(`${process.cwd()}/../../../docs/${pkg}/${version}.api.json`, 'utf8'));
},
});
console.log('Generated all indices.');

View File

@@ -3,6 +3,7 @@
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { ImageResponse } from '@vercel/og';
import type { NextRequest } from 'next/server';
import { resolvePackageName } from '~/util/resolvePackageName';
export const runtime = 'edge';
@@ -87,7 +88,7 @@ export async function GET(request: NextRequest) {
const hasMethods = searchParams.has('methods');
const hasProps = searchParams.has('props');
const hasMembers = searchParams.has('members');
const pkg = hasPkg ? searchParams.get('pkg') : '';
const pkg = hasPkg ? resolvePackageName(searchParams.get('pkg')!) : '';
const kind = hasKind ? searchParams.get('kind')! : 'Method';
const name = hasName ? searchParams.get('name')!.slice(0, 100) : 'My default name which is super long to overflow';
const methods = hasMethods ? searchParams.get('methods') : '';
@@ -103,7 +104,7 @@ export async function GET(request: NextRequest) {
tw="flex flex-row bg-[#181818] h-full w-full p-24"
>
<div tw="flex flex-col mx-auto h-full text-white">
<div tw="flex flex-row text-4xl text-gray-400">@discordjs/{pkg}</div>
<div tw="flex flex-row text-4xl text-gray-400">{pkg}</div>
<div tw="flex flex-col justify-between h-full w-full pt-14">
<div tw="flex flex-row items-center max-w-full">
<span tw="mr-6">{resolveIcon(kind as keyof typeof ApiItemKind)}</span>

View File

@@ -1,36 +1,23 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { connect } from '@planetscale/database';
import { cache } from 'react';
import { N_RECENT_VERSIONS } from '~/util/constants';
import { sql } from '@vercel/postgres';
const sql = connect({
url: process.env.DATABASE_URL!,
async fetch(url, init) {
delete init?.cache;
return fetch(url, { ...init, next: { revalidate: 3_600 } });
},
});
export const fetchVersions = cache(async (packageName: string): Promise<string[]> => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV || process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
export const fetchVersions = async (packageName: string): Promise<string[]> => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' || process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return ['main'];
}
try {
const { rows } = await sql.execute('select version from documentation where name = ? order by version desc', [
packageName,
]);
const { rows } = await sql`select version from documentation where name = ${packageName} order by version desc`;
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows.map((row) => row.version).slice(0, N_RECENT_VERSIONS);
return rows.map((row) => row.version);
} catch {
return [];
}
});
};
export const fetchModelJSON = cache(async (packageName: string, version: string) => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV) {
export const fetchModelJSON = async (packageName: string, version: string) => {
if (process.env.NEXT_PUBLIC_LOCAL_DEV === 'true') {
try {
const res = await readFile(
join(process.cwd(), '..', '..', 'packages', packageName, 'docs', 'docs.api.json'),
@@ -45,27 +32,21 @@ export const fetchModelJSON = cache(async (packageName: string, version: string)
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
try {
const { rows } = await sql.execute('select data from documentation where name = ? and version = ?', [
packageName,
'main',
]);
const { rows } = await sql`select url from documentation where name = ${packageName} and version = ${'main'}`;
const res = await fetch(rows[0]?.url ?? '');
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows[0]?.data ?? null;
return await res.json();
} catch {
return null;
}
}
try {
const { rows } = await sql.execute('select data from documentation where name = ? and version = ?', [
packageName,
version,
]);
const { rows } = await sql`select url from documentation where name = ${packageName} and version = ${version}`;
const res = await fetch(rows[0]?.url ?? '');
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows[0]?.data ?? null;
return await res.json();
} catch {
return null;
}
});
};

View File

@@ -0,0 +1,20 @@
export default function Loading() {
return (
<div className="relative top-6 mx-4 min-h-xl flex flex-col items-center justify-center gap-4">
<svg
className="h-9 w-9 animate-spin text-black dark:text-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75 dark:opacity-100"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
/>
</svg>
<div className="text-lg font-medium">Loading...</div>
</div>
);
}

View File

@@ -119,10 +119,14 @@ export async function generateMetadata({ params }: { params: ItemRouteParams })
}
export async function generateStaticParams({ params: { package: packageName, version } }: { params: ItemRouteParams }) {
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return [];
}
const modelJSON = await fetchModelJSON(packageName, version);
if (!modelJSON) {
return [{ package: packageName, version, item: '' }];
return [];
}
const model = addPackageToModel(new ApiModel(), modelJSON);
@@ -131,7 +135,7 @@ export async function generateStaticParams({ params: { package: packageName, ver
const entry = pkg?.entryPoints[0];
if (!entry) {
return [{ package: packageName, version, item: '' }];
return [];
}
return entry.members.map((member: ApiItem) => ({
@@ -168,7 +172,7 @@ export default async function Page({ params }: { params: ItemRouteParams }) {
}
return (
<div className="relative top-6">
<div className="relative">
<Member member={member} />
</div>
);

View File

@@ -2,14 +2,15 @@ import type { ApiFunction, ApiItem } from '@discordjs/api-extractor-model';
import { ApiModel } from '@discordjs/api-extractor-model';
import dynamic from 'next/dynamic';
import { notFound } from 'next/navigation';
import { cache, type PropsWithChildren } from 'react';
import type { PropsWithChildren } from 'react';
import { fetchModelJSON, fetchVersions } from '~/app/docAPI';
import { CmdKDialog } from '~/components/CmdK';
import { Nav } from '~/components/Nav';
import { Outline } from '~/components/Outline';
import type { SidebarSectionItemData } from '~/components/Sidebar';
import { resolveItemURI } from '~/components/documentation/util';
import { addPackageToModel } from '~/util/addPackageToModel';
import { PACKAGES } from '~/util/constants';
import { N_RECENT_VERSIONS, PACKAGES } from '~/util/constants';
import { Providers } from './providers';
export const revalidate = 3_600;
@@ -23,11 +24,15 @@ interface VersionRouteParams {
}
export const generateStaticParams = async () => {
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return [];
}
const params: VersionRouteParams[] = [];
await Promise.all(
PACKAGES.slice(1).map(async (packageName) => {
const versions = await fetchVersions(packageName);
PACKAGES.map(async (packageName) => {
const versions = (await fetchVersions(packageName)).slice(1, N_RECENT_VERSIONS);
params.push(...versions.map((version) => ({ package: packageName, version })));
}),
@@ -36,14 +41,14 @@ export const generateStaticParams = async () => {
return params;
};
const serializeIntoSidebarItemData = cache((item: ApiItem) => {
const serializeIntoSidebarItemData = (item: ApiItem) => {
return {
kind: item.kind,
name: item.displayName,
href: resolveItemURI(item),
overloadIndex: 'overloadIndex' in item ? (item.overloadIndex as number) : undefined,
} as SidebarSectionItemData;
});
};
export default async function PackageLayout({ children, params }: PropsWithChildren<{ params: VersionRouteParams }>) {
const modelJSON = await fetchModelJSON(params.package, params.version);
@@ -80,15 +85,17 @@ export default async function PackageLayout({ children, params }: PropsWithChild
<Providers>
<main className="mx-auto max-w-7xl px-4 lg:max-w-full">
<Header />
<div className="relative top-2.5 mx-auto max-w-7xl gap-6 lg:max-w-full lg:flex">
<div className="relative top-6.5 mx-auto max-w-7xl gap-6 lg:max-w-full lg:flex">
<div className="lg:sticky lg:top-23 lg:h-[calc(100vh_-_105px)]">
<Nav members={members.map((member) => serializeIntoSidebarItemData(member))} versions={versions} />
</div>
<div className="mx-auto max-w-5xl min-w-xs w-full pb-10">
<div className="relative top-4.5 mx-auto max-w-5xl min-w-xs w-full pb-10">
{children}
<Footer />
</div>
<Outline />
</div>
</main>
<CmdKDialog />

View File

@@ -15,10 +15,6 @@ const loadREADME = cache(async (packageName: string) => {
return readFile(join(process.cwd(), 'src', 'assets', 'readme', packageName, 'home-README.md'), 'utf8');
});
export async function generateStaticParams({ params }: { params: VersionRouteParams }) {
return [{ package: params.package, version: params.version }];
}
export default async function Page({ params }: { params: VersionRouteParams }) {
const readmeSource = await loadREADME(params.package);
const { content } = await compileMDX({

View File

@@ -4,13 +4,16 @@ import type { PropsWithChildren } from 'react';
import { CmdKProvider } from '~/contexts/cmdK';
import { MemberProvider } from '~/contexts/member';
import { NavProvider } from '~/contexts/nav';
import { OutlineProvider } from '~/contexts/outline';
export function Providers({ children }: PropsWithChildren) {
return (
<NavProvider>
<MemberProvider>
<CmdKProvider>{children}</CmdKProvider>
</MemberProvider>
<OutlineProvider>
<MemberProvider>
<CmdKProvider>{children}</CmdKProvider>
</MemberProvider>
</OutlineProvider>
</NavProvider>
);
}

View File

@@ -28,7 +28,9 @@ export function Badges({ item }: { readonly item: ApiDocumentedItem }) {
const isAbstract = ApiAbstractMixin.isBaseClassOf(item) && item.isAbstract;
const isDeprecated = Boolean(item.tsdocComment?.deprecatedBlock);
return (
const isAny = isStatic || isProtected || isReadonly || isAbstract || isDeprecated;
return isAny ? (
<div className="flex flex-row gap-1 md:ml-7">
{isDeprecated ? <Badge color={BadgeColor.Danger}>Deprecated</Badge> : null}
{isProtected ? <Badge>Protected</Badge> : null}
@@ -36,5 +38,5 @@ export function Badges({ item }: { readonly item: ApiDocumentedItem }) {
{isAbstract ? <Badge>Abstract</Badge> : null}
{isReadonly ? <Badge>Readonly</Badge> : null}
</div>
);
) : null;
}

View File

@@ -1,26 +1,22 @@
import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model';
import type { Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptTokenKind } from '@discordjs/api-extractor-model';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants';
import { DocumentationLink } from './DocumentationLink';
import { ItemLink } from './ItemLink';
import { resolveItemURI } from './documentation/util';
import { resolveCanonicalReference, resolveItemURI } from './documentation/util';
export interface ExcerptTextProps {
/**
* The tokens to render.
*/
readonly excerpt: Excerpt;
/**
* The model to resolve item references from.
*/
readonly model: ApiModel;
}
/**
* A component that renders excerpt tokens from an api item.
*/
export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
export function ExcerptText({ excerpt }: ExcerptTextProps) {
return (
<span>
{excerpt.spannedTokens.map((token, idx) => {
@@ -53,20 +49,18 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
);
}
const item = token.canonicalReference
? model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem
: null;
const resolved = token.canonicalReference ? resolveCanonicalReference(token.canonicalReference) : null;
if (!item) {
if (!resolved) {
return token.text;
}
return (
<ItemLink
className="text-blurple"
itemURI={resolveItemURI(item)}
key={`${item.displayName}-${item.containerKey}-${idx}`}
packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
itemURI={resolveItemURI(resolved.item)}
key={`${resolved.item.displayName}-${resolved.item.containerKey}-${idx}`}
packageName={resolved.package}
>
{token.text}
</ItemLink>

View File

@@ -17,7 +17,10 @@ export function InstallButton() {
return (
<button
className={buttonVariants({ variant: 'secondary', className: 'cursor-copy font-mono' })}
className={buttonVariants({
variant: 'secondary',
className: 'cursor-copy font-mono',
})}
onClick={() => {
setInteracted(true);
copyToClipboard('npm install discord.js');

View File

@@ -35,10 +35,12 @@ export function Nav({
universal
>
<div className="flex flex-col gap-4 p-3">
<PackageSelect />
<VersionSelect versions={versions} />
<div className="flex flex-col gap-4">
<PackageSelect />
<VersionSelect versions={versions} />
</div>
<Sidebar members={members} />
</div>
<Sidebar members={members} />
</Scrollbars>
</nav>
);

View File

@@ -1,23 +1,32 @@
'use client';
import { useOutline } from '~/contexts/outline';
import { Scrollbars } from './Scrollbars';
import type { TableOfContentsSerialized } from './TableOfContentItems';
import { TableOfContentItems } from './TableOfContentItems';
export function Outline({ members }: { readonly members: TableOfContentsSerialized[] }) {
export function Outline() {
const { members } = useOutline();
if (!members) {
return null;
}
return (
<aside className="fixed bottom-0 right-0 top-[50px] z-20 hidden h-[calc(100vh_-_65px)] w-64 border-l border-light-800 bg-white pr-2 xl:block dark:border-dark-100 dark:bg-dark-600">
<Scrollbars
autoHide
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<TableOfContentItems serializedMembers={members} />
</Scrollbars>
</aside>
<div className="lg:sticky lg:top-23 lg:h-[calc(100vh_-_105px)]">
<aside className="fixed bottom-4 left-4 right-4 top-22 z-20 mx-auto hidden max-w-5xl border border-light-900 rounded-md bg-white/75 shadow backdrop-blur-md lg:sticky lg:block lg:h-full lg:max-w-xs lg:min-w-xs lg:w-full dark:border-dark-100 dark:bg-dark-600/75">
<Scrollbars
autoHide
className="[&>div]:overscroll-none"
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<TableOfContentItems serializedMembers={members} />
</Scrollbars>
</aside>
</div>
);
}

View File

@@ -15,7 +15,10 @@ export default function OverloadSwitcher({
methodName,
overloads,
children,
}: PropsWithChildren<{ readonly methodName: string; readonly overloads: ReactNode[] }>) {
}: PropsWithChildren<{
readonly methodName: string;
readonly overloads: ReactNode[];
}>) {
const [hash, setHash] = useState(() => (typeof window === 'undefined' ? '' : window.location.hash));
const hashChangeHandler = useCallback(() => {
setHash(window.location.hash);

View File

@@ -12,7 +12,11 @@ export default function PackageSelect() {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const packageMenu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
const packageMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const packageMenuItems = useMemo(
() =>

View File

@@ -17,7 +17,7 @@ export function ParameterTable({ item }: { readonly item: ApiDocumentedItem & Ap
() =>
params.map((param) => ({
Name: param.isRest ? `...${param.name}` : param.name,
Type: <ExcerptText excerpt={param.parameterTypeExcerpt} model={item.getAssociatedModel()!} />,
Type: <ExcerptText excerpt={param.parameterTypeExcerpt} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.description ? <TSDoc item={item} tsdoc={param.description} /> : 'None',
})),

View File

@@ -32,9 +32,7 @@ export function Property({
>
{`${item.displayName}${item.isOptional ? '?' : ''}`}
<span>:</span>
{item.propertyTypeExcerpt.text ? (
<ExcerptText excerpt={item.propertyTypeExcerpt} model={item.getAssociatedModel()!} />
) : null}
{item.propertyTypeExcerpt.text ? <ExcerptText excerpt={item.propertyTypeExcerpt} /> : null}
</CodeHeading>
</div>
{hasSummary || inheritedFrom ? (

View File

@@ -87,7 +87,7 @@ export function Sidebar({ members }: { readonly members: SidebarSectionItemData[
const groupItems = useMemo(() => groupMembers(members), [members]);
return (
<div className="flex flex-col gap-3 p-3">
<div className="flex flex-col gap-4">
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
@@ -99,7 +99,7 @@ export function Sidebar({ members }: { readonly members: SidebarSectionItemData[
>
{groupItems[group].map((member, index) => (
<ItemLink
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l first:mt-1 p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
decodeURIComponent(segment ?? '') === member.href
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'

View File

@@ -1,10 +1,10 @@
import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model';
import type { Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptText } from './ExcerptText';
export function SignatureText({ excerpt, model }: { readonly excerpt: Excerpt; readonly model: ApiModel }) {
export function SignatureText({ excerpt }: { readonly excerpt: Excerpt }) {
return (
<h4 className="break-all text-lg font-bold font-mono">
<ExcerptText excerpt={excerpt} model={model} />
<ExcerptText excerpt={excerpt} />
</h4>
);
}

View File

@@ -11,21 +11,20 @@ const rowElements = {
};
export function TypeParamTable({ item }: { readonly item: ApiTypeParameterListMixin }) {
const model = item.getAssociatedModel()!;
const rows = useMemo(
() =>
item.typeParameters.map((typeParam) => ({
Name: typeParam.name,
Constraints: <ExcerptText excerpt={typeParam.constraintExcerpt} model={model} />,
Constraints: <ExcerptText excerpt={typeParam.constraintExcerpt} />,
Optional: typeParam.isOptional ? 'Yes' : 'No',
Default: <ExcerptText excerpt={typeParam.defaultTypeExcerpt} model={model} />,
Default: <ExcerptText excerpt={typeParam.defaultTypeExcerpt} />,
Description: typeParam.tsdocTypeParamBlock ? (
<TSDoc item={item} tsdoc={typeParam.tsdocTypeParamBlock.content} />
) : (
'None'
),
})),
[item, model],
[item],
);
return (

View File

@@ -7,14 +7,18 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
const isDev = process.env.NEXT_PUBLIC_LOCAL_DEV ?? process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview';
const isDev = process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' ?? process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview';
export default function VersionSelect({ versions }: { readonly versions: string[] }) {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0];
const versionMenu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
const versionMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const versionMenuItems = useMemo(
() =>

View File

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

View File

@@ -9,8 +9,6 @@ export function HierarchyText({
readonly item: ApiClass | ApiInterface;
readonly type: 'Extends' | 'Implements';
}) {
const model = item.getAssociatedModel()!;
if (
(item.kind === ApiItemKind.Class &&
(item as ApiClass).extendsType === undefined &&
@@ -50,7 +48,7 @@ export function HierarchyText({
<div className="flex flex-row place-items-center gap-4" key={`${type}-${idx}`}>
<h3 className="text-xl font-bold">{type}</h3>
<span className="break-all font-mono space-y-2">
<ExcerptText excerpt={excerpt} model={model} />
<ExcerptText excerpt={excerpt} />
</span>
</div>
))}

View File

@@ -13,7 +13,9 @@ export function Block({ children, title }: PropsWithChildren<{ readonly title: s
export function ExampleBlock({
children,
exampleIndex,
}: PropsWithChildren<{ readonly exampleIndex?: number | undefined }>): JSX.Element {
}: PropsWithChildren<{
readonly exampleIndex?: number | undefined;
}>): JSX.Element {
return <Block title={`Example ${exampleIndex ? exampleIndex : ''}`}>{children}</Block>;
}

View File

@@ -4,6 +4,8 @@ import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import type { Route } from 'next';
import Link from 'next/link';
import { Fragment, useCallback, type ReactNode } from 'react';
import { DocumentationLink } from '~/components/DocumentationLink';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
import { ItemLink } from '../../ItemLink';
import { SyntaxHighlighter } from '../../SyntaxHighlighter';
import { resolveItemURI } from '../util';
@@ -32,10 +34,24 @@ export function TSDoc({ item, tsdoc }: { readonly item: ApiItem; readonly tsdoc:
const { codeDestination, urlDestination, linkText } = tsdoc as DocLinkTag;
if (codeDestination) {
// TODO: Real fix in api-extractor needed
const currentItem = item.getAssociatedPackage();
const foundItem = item.getAssociatedModel()?.resolveDeclarationReference(codeDestination, currentItem)
.resolvedApiItem;
if (
!codeDestination.importPath &&
!codeDestination.packageName &&
codeDestination.memberReferences.length === 1 &&
codeDestination.memberReferences[0]!.memberIdentifier &&
codeDestination.memberReferences[0]!.memberIdentifier.identifier in BuiltinDocumentationLinks
) {
const typeName = codeDestination.memberReferences[0]!.memberIdentifier.identifier;
const href = BuiltinDocumentationLinks[typeName as keyof typeof BuiltinDocumentationLinks];
return (
<DocumentationLink key={`${typeName}-${idx}`} href={href}>
{typeName}
</DocumentationLink>
);
}
const declarationReference = item.getAssociatedModel()?.resolveDeclarationReference(codeDestination, item);
const foundItem = declarationReference?.resolvedApiItem;
if (!foundItem) return null;
@@ -44,6 +60,7 @@ export function TSDoc({ item, tsdoc }: { readonly item: ApiItem; readonly tsdoc:
className="rounded font-mono text-blurple outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
itemURI={resolveItemURI(foundItem)}
key={idx}
packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
>
{linkText ?? foundItem.displayName}
</ItemLink>
@@ -92,7 +109,7 @@ export function TSDoc({ item, tsdoc }: { readonly item: ApiItem; readonly tsdoc:
);
return (
<div className="flex flex-col space-y-2">
<div className="flex flex-col gap-2">
{comment.deprecatedBlock ? (
<DeprecatedBlock>{createNode(comment.deprecatedBlock.content)}</DeprecatedBlock>
) : null}

View File

@@ -1,4 +1,4 @@
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { ApiItemKind, Meaning } from '@discordjs/api-extractor-model';
import type {
ApiItem,
ApiItemContainerMixin,
@@ -10,11 +10,25 @@ import type {
ApiParameterListMixin,
ApiEvent,
} from '@discordjs/api-extractor-model';
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference';
import { METHOD_SEPARATOR, OVERLOAD_SEPARATOR } from '~/util/constants';
import { resolveMembers } from '~/util/members';
import { resolveParameters } from '~/util/model';
import type { TableOfContentsSerialized } from '../TableOfContentItems';
export type ApiItemLike = {
[K in keyof ApiItem]?: K extends 'displayName' | 'kind'
? ApiItem[K]
: K extends 'parent'
? ApiItemLike | undefined
: ApiItem[K] | undefined;
};
interface ResolvedCanonicalReference {
item: ApiItemLike;
package: string;
}
export function hasProperties(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(
({ item: member }) => member.kind === ApiItemKind.Property || member.kind === ApiItemKind.PropertySignature,
@@ -31,12 +45,65 @@ export function hasEvents(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(({ item: member }) => member.kind === ApiItemKind.Event);
}
export function resolveItemURI(item: ApiItem): string {
export function resolveItemURI(item: ApiItemLike): string {
return !item.parent || item.parent.kind === ApiItemKind.EntryPoint
? `${item.displayName}${OVERLOAD_SEPARATOR}${item.kind}`
: `${item.parent.displayName}${OVERLOAD_SEPARATOR}${item.parent.kind}${METHOD_SEPARATOR}${item.displayName}`;
}
export function resolveCanonicalReference(canonicalReference: DeclarationReference): ResolvedCanonicalReference | null {
if (
canonicalReference.source &&
'packageName' in canonicalReference.source &&
canonicalReference.symbol?.componentPath &&
canonicalReference.symbol.meaning
)
return {
package: canonicalReference.source.unscopedPackageName,
item: {
kind: mapMeaningToKind(canonicalReference.symbol.meaning as unknown as Meaning),
displayName: canonicalReference.symbol.componentPath.component.toString(),
containerKey: `|${
canonicalReference.symbol.meaning
}|${canonicalReference.symbol.componentPath.component.toString()}`,
},
};
return null;
}
function mapMeaningToKind(meaning: Meaning): ApiItemKind {
switch (meaning) {
case Meaning.CallSignature:
return ApiItemKind.CallSignature;
case Meaning.Class:
return ApiItemKind.Class;
case Meaning.ComplexType:
throw new Error('Not a valid canonicalReference: Meaning.ComplexType');
case Meaning.ConstructSignature:
return ApiItemKind.ConstructSignature;
case Meaning.Constructor:
return ApiItemKind.Constructor;
case Meaning.Enum:
return ApiItemKind.Enum;
case Meaning.Event:
return ApiItemKind.Event;
case Meaning.Function:
return ApiItemKind.Function;
case Meaning.IndexSignature:
return ApiItemKind.IndexSignature;
case Meaning.Interface:
return ApiItemKind.Interface;
case Meaning.Member:
return ApiItemKind.Property;
case Meaning.Namespace:
return ApiItemKind.Namespace;
case Meaning.TypeAlias:
return ApiItemKind.TypeAlias;
case Meaning.Variable:
return ApiItemKind.Variable;
}
}
export function memberPredicate(
item: ApiItem,
): item is ApiEvent | ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature {

View File

@@ -1,6 +1,5 @@
import type { ApiClass, ApiConstructor } from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
// import { Outline } from '../Outline';
import { Badges } from '../Badges';
import { Documentation } from '../documentation/Documentation';
import { HierarchyText } from '../documentation/HierarchyText';
@@ -8,13 +7,16 @@ import { Members } from '../documentation/Members';
import { ObjectHeader } from '../documentation/ObjectHeader';
import { ConstructorSection } from '../documentation/section/ConstructorSection';
import { TypeParameterSection } from '../documentation/section/TypeParametersSection';
// import { serializeMembers } from '../documentation/util';
import { serializeMembers } from '../documentation/util';
import { OutlineSetter } from './OutlineSetter';
export function Class({ clazz }: { readonly clazz: ApiClass }) {
const constructor = clazz.members.find((member) => member.kind === ApiItemKind.Constructor) as
| ApiConstructor
| undefined;
const outlineMembers = serializeMembers(clazz);
return (
<Documentation>
<Badges item={clazz} />
@@ -24,7 +26,7 @@ export function Class({ clazz }: { readonly clazz: ApiClass }) {
{clazz.typeParameters.length ? <TypeParameterSection item={clazz} /> : null}
{constructor ? <ConstructorSection item={constructor} /> : null}
<Members item={clazz} />
{/* <Outline members={serializeMembers(clazz)} /> */}
<OutlineSetter members={outlineMembers} />
</Documentation>
);
}

View File

@@ -1,20 +1,22 @@
import type { ApiInterface } from '@discordjs/api-extractor-model';
// import { Outline } from '../Outline';
import { Documentation } from '../documentation/Documentation';
import { HierarchyText } from '../documentation/HierarchyText';
import { Members } from '../documentation/Members';
import { ObjectHeader } from '../documentation/ObjectHeader';
import { TypeParameterSection } from '../documentation/section/TypeParametersSection';
// import { serializeMembers } from '../documentation/util';
import { serializeMembers } from '../documentation/util';
import { OutlineSetter } from './OutlineSetter';
export function Interface({ item }: { readonly item: ApiInterface }) {
const outlineMembers = serializeMembers(item);
return (
<Documentation>
<ObjectHeader item={item} />
<HierarchyText item={item} type="Extends" />
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}
<Members item={item} />
{/* <Outline members={serializeMembers(item)} /> */}
<OutlineSetter members={outlineMembers} />
</Documentation>
);
}

View File

@@ -0,0 +1,19 @@
'use client';
import { useEffect, type PropsWithChildren } from 'react';
import { useOutline } from '~/contexts/outline';
import type { TableOfContentsSerialized } from '../TableOfContentItems';
export function OutlineSetter({ members }: PropsWithChildren<{ readonly members: TableOfContentsSerialized[] }>) {
const { setMembers } = useOutline();
useEffect(() => {
setMembers(members);
return () => {
setMembers(null);
};
}, [members, setMembers]);
return null;
}

View File

@@ -14,9 +14,7 @@ export function EnumMember({ member }: { readonly member: ApiEnumMember }) {
>
{member.name}
<span>=</span>
{member.initializerExcerpt ? (
<SignatureText excerpt={member.initializerExcerpt} model={member.getAssociatedModel()!} />
) : null}
{member.initializerExcerpt ? <SignatureText excerpt={member.initializerExcerpt} /> : null}
</CodeHeading>
{member.tsdocComment ? <TSDoc item={member} tsdoc={member.tsdocComment.summarySection} /> : null}
</div>

View File

@@ -1,37 +1,29 @@
import type { ApiFunction } from '@discordjs/api-extractor-model';
import dynamic from 'next/dynamic';
import { Header } from '../../documentation/Header';
import { Documentation } from '~/components/documentation/Documentation';
import { ObjectHeader } from '~/components/documentation/ObjectHeader';
import { FunctionBody } from './FunctionBody';
const OverloadSwitcher = dynamic(async () => import('../../OverloadSwitcher'));
export function Function({ item }: { readonly item: ApiFunction }) {
const header = (
<Header
kind={item.kind}
name={item.name}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
/>
);
if (item.getMergedSiblings().length > 1) {
const overloads = item
.getMergedSiblings()
.map((sibling, idx) => <FunctionBody item={sibling as ApiFunction} key={`${sibling.displayName}-${idx}`} />);
return (
<div>
{header}
<Documentation>
<ObjectHeader item={item} />
<OverloadSwitcher methodName={item.displayName} overloads={overloads} />
</div>
</Documentation>
);
}
return (
<div>
{header}
<Documentation>
<ObjectHeader item={item} />
<FunctionBody item={item} />
</div>
</Documentation>
);
}

View File

@@ -1,8 +1,5 @@
import type { ApiFunction } from '@discordjs/api-extractor-model';
import { SyntaxHighlighter } from '../../SyntaxHighlighter';
import { Documentation } from '../../documentation/Documentation';
import { ParameterSection } from '../../documentation/section/ParametersSection';
import { SummarySection } from '../../documentation/section/SummarySection';
import { TypeParameterSection } from '../../documentation/section/TypeParametersSection';
export interface FunctionBodyProps {
@@ -12,12 +9,9 @@ export interface FunctionBodyProps {
export function FunctionBody({ item }: { readonly item: ApiFunction }) {
return (
<Documentation>
{/* @ts-expect-error async component */}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
<>
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}
{item.parameters.length ? <ParameterSection item={item} /> : null}
</Documentation>
</>
);
}

View File

@@ -22,7 +22,7 @@ export function MethodHeader({ method }: { readonly method: ApiMethod | ApiMetho
>
{`${method.name}(${parametersString(method)})`}
<span>:</span>
<ExcerptText excerpt={method.returnTypeExcerpt} model={method.getAssociatedModel()!} />
<ExcerptText excerpt={method.returnTypeExcerpt} />
</CodeHeading>
</div>
</div>

View File

@@ -10,7 +10,10 @@ import {
useMemo,
} from 'react';
export const NavContext = createContext<{ opened: boolean; setOpened: Dispatch<SetStateAction<boolean>> }>({
export const NavContext = createContext<{
opened: boolean;
setOpened: Dispatch<SetStateAction<boolean>>;
}>({
opened: false,
setOpened: (_) => {},
});

View File

@@ -0,0 +1,22 @@
'use client';
import { createContext, useContext, useMemo, useState } from 'react';
import type { PropsWithChildren, Dispatch, SetStateAction } from 'react';
import type { TableOfContentsSerialized } from '~/components/TableOfContentItems';
export const OutlineContext = createContext<{
members: TableOfContentsSerialized[] | null | undefined;
setMembers: Dispatch<SetStateAction<TableOfContentsSerialized[] | null | undefined>>;
}>({ members: undefined, setMembers: (_) => {} });
export const OutlineProvider = ({ children }: PropsWithChildren) => {
const [members, setMembers] = useState<TableOfContentsSerialized[] | null | undefined>(undefined);
const value = useMemo(() => ({ members, setMembers }), [members]);
return <OutlineContext.Provider value={value}>{children}</OutlineContext.Provider>;
};
export function useOutline() {
return useContext(OutlineContext);
}

View File

@@ -1,26 +1,15 @@
import { connect } from '@planetscale/database';
import { get } from '@vercel/edge-config';
import { sql } from '@vercel/postgres';
import { NextResponse, type NextRequest } from 'next/server';
import { PACKAGES } from './util/constants';
const sql = connect({
url: process.env.DATABASE_URL!,
async fetch(url, init) {
delete init?.cache;
return fetch(url, { ...init, next: { revalidate: 3_600 } });
},
});
async function fetchLatestVersion(packageName: string): Promise<string> {
if (process.env.NEXT_PUBLIC_LOCAL_DEV || process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
if (process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' || process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview') {
return 'main';
}
const { rows } = await sql.execute('select version from documentation where name = ? order by version desc', [
packageName,
]);
const { rows } = await sql`select version from documentation where name = ${packageName} order by version desc`;
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows.map((row) => row.version).at(1) ?? 'main';
}

View File

@@ -1,27 +1,9 @@
import type { ApiModel, ApiPackage } from '@discordjs/api-extractor-model';
import { ApiItem } from '@discordjs/api-extractor-model';
import { TSDocConfiguration } from '@microsoft/tsdoc';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import { cache } from 'react';
import { ApiPackage } from '@discordjs/api-extractor-model';
import type { ApiModel } from '@discordjs/api-extractor-model';
export const addPackageToModel = cache((model: ApiModel, data: any) => {
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;
}
export const addPackageToModel = (model: ApiModel, data: any) => {
const apiPackage = ApiPackage.loadFromJson(data);
model.addMember(apiPackage);
return model;
});
};

View File

@@ -1,11 +1,10 @@
import { ApiModel, ApiFunction } from '@discordjs/api-extractor-model';
import { cache } from 'react';
import { fetchModelJSON } from '~/app/docAPI';
import { addPackageToModel } from './addPackageToModel';
import { OVERLOAD_SEPARATOR, PACKAGES } from './constants';
import { findMember, findMemberByKey } from './model';
export const fetchMember = cache(async (packageName: string, branchName: string, item?: string) => {
export const fetchMember = async (packageName: string, branchName: string, item?: string) => {
if (!PACKAGES.includes(packageName)) {
return null;
}
@@ -16,26 +15,14 @@ export const fetchMember = cache(async (packageName: string, branchName: string,
const model = new ApiModel();
if (branchName === 'main') {
const modelJSONFiles = await Promise.all(PACKAGES.map(async (pkg) => fetchModelJSON(pkg, branchName)));
const modelJSON = await fetchModelJSON(packageName, 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);
if (!modelJSON) {
return null;
}
addPackageToModel(model, modelJSON);
const [memberName, overloadIndex] = decodeURIComponent(item).split(OVERLOAD_SEPARATOR);
// eslint-disable-next-line prefer-const
@@ -45,4 +32,4 @@ export const fetchMember = cache(async (packageName: string, branchName: string,
}
return memberName && containerKey ? findMemberByKey(model, packageName, containerKey) ?? null : null;
});
};

View File

@@ -6,21 +6,21 @@ import type {
Excerpt,
} from '@discordjs/api-extractor-model';
import type { DocSection } from '@microsoft/tsdoc';
import { cache } from 'react';
import { resolvePackageName } from './resolvePackageName';
export const findMemberByKey = cache((model: ApiModel, packageName: string, containerKey: string) => {
const pkg = model.tryGetPackageByName(packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`)!;
export const findMemberByKey = (model: ApiModel, packageName: string, containerKey: string) => {
const pkg = model.tryGetPackageByName(resolvePackageName(packageName))!;
return (pkg.members[0] as ApiEntryPoint).tryGetMemberByKey(containerKey);
});
};
export const findMember = cache((model: ApiModel, packageName: string, memberName: string | undefined) => {
export const findMember = (model: ApiModel, packageName: string, memberName: string | undefined) => {
if (!memberName) {
return undefined;
}
const pkg = model.tryGetPackageByName(packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`)!;
const pkg = model.tryGetPackageByName(resolvePackageName(packageName))!;
return pkg.entryPoints[0]?.findMembersByName(memberName)[0];
});
};
interface ResolvedParameter {
description?: DocSection | undefined;

View File

@@ -0,0 +1,3 @@
export function resolvePackageName(packageName: string) {
return packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`;
}

View File

@@ -1,8 +0,0 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": {
"main": false
}
}
}

View File

@@ -49,7 +49,7 @@
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@commitlint/cli": "^18.4.0",
"@commitlint/cli": "^18.4.1",
"@commitlint/config-angular": "^18.4.0",
"@favware/cliff-jumper": "^2.2.1",
"@favware/npm-deprecate": "^1.0.7",
@@ -63,6 +63,7 @@
"is-ci": "^3.0.1",
"lint-staged": "^15.1.0",
"lodash.merge": "^4.6.2",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -0,0 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +0,0 @@
export * from '../../.lintstagedrc.json' assert { type: 'json' };

View File

@@ -0,0 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -1 +0,0 @@
export * from '../../.prettierrc.json' assert { type: 'json' };

View File

@@ -3,24 +3,44 @@ import { formatTag } from '../src/index.js';
describe('Format Tag', () => {
test('GIVEN tag with a prefix THEN format tag to not contain the prefix', () => {
expect(formatTag('@discordjs/rest@0.4.0')).toEqual({ isSubpackage: true, package: 'rest', semver: '0.4.0' });
expect(formatTag('@discordjs/rest@0.4.0')).toEqual({
isSubpackage: true,
package: 'rest',
semver: '0.4.0',
});
expect(formatTag('@discordjs/collection@0.6.0')).toEqual({
isSubpackage: true,
package: 'collection',
semver: '0.6.0',
});
expect(formatTag('@discordjs/proxy@0.1.0')).toEqual({ isSubpackage: true, package: 'proxy', semver: '0.1.0' });
expect(formatTag('@discordjs/proxy@0.1.0')).toEqual({
isSubpackage: true,
package: 'proxy',
semver: '0.1.0',
});
expect(formatTag('@discordjs/builders@0.13.0')).toEqual({
isSubpackage: true,
package: 'builders',
semver: '0.13.0',
});
expect(formatTag('@discordjs/voice@0.9.0')).toEqual({ isSubpackage: true, package: 'voice', semver: '0.9.0' });
expect(formatTag('@discordjs/voice@0.9.0')).toEqual({
isSubpackage: true,
package: 'voice',
semver: '0.9.0',
});
});
test('GIVEN tag with no prefix THEN return tag', () => {
expect(formatTag('13.5.1')).toEqual({ isSubpackage: false, package: 'discord.js', semver: '13.5.1' });
expect(formatTag('13.7.0')).toEqual({ isSubpackage: false, package: 'discord.js', semver: '13.7.0' });
expect(formatTag('13.5.1')).toEqual({
isSubpackage: false,
package: 'discord.js',
semver: '13.5.1',
});
expect(formatTag('13.7.0')).toEqual({
isSubpackage: false,
package: 'discord.js',
semver: '13.7.0',
});
expect(formatTag('create-discord-bot@1.0.0')).toEqual({
isSubpackage: false,
package: 'create-discord-bot',

View File

@@ -43,7 +43,8 @@
"@actions/core": "^1.10.1",
"@actions/glob": "^0.4.0",
"@discordjs/scripts": "workspace:^",
"@planetscale/database": "^1.11.0",
"@vercel/blob": "^0.15.0",
"@vercel/postgres": "^0.5.1",
"meilisearch": "^0.35.0",
"tslib": "^2.6.2",
"undici": "5.27.2"
@@ -55,7 +56,7 @@
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -2,8 +2,8 @@ import { readFile } from 'node:fs/promises';
import process from 'node:process';
import { getInput, setFailed } from '@actions/core';
import { create } from '@actions/glob';
import { connect } from '@planetscale/database';
import { fetch } from 'undici';
import { put } from '@vercel/blob';
import { createPool } from '@vercel/postgres';
if (!process.env.DATABASE_URL) {
setFailed('DATABASE_URL is not set');
@@ -12,9 +12,8 @@ if (!process.env.DATABASE_URL) {
const pkg = getInput('package') || '*';
const version = getInput('version') || 'main';
const sql = connect({
fetch,
url: process.env.DATABASE_URL!,
const pool = createPool({
connectionString: process.env.DATABASE_URL,
});
const globber = await create(`packages/${pkg}/docs/docs.api.json`);
@@ -22,9 +21,18 @@ for await (const file of globber.globGenerator()) {
const data = await readFile(file, 'utf8');
try {
console.log(`Uploading ${file} with ${version}...`);
await sql.execute('replace into documentation (version, data) values (?, ?)', [version, data]);
const json = JSON.parse(data);
const name = json.name ?? json.n;
const { url } = await put(`${name.replace('@discordjs/', '')}/${version}.json`, data, {
access: 'public',
addRandomSuffix: false,
});
await pool.sql`insert into documentation (name, version, url) values (${name.replace(
'@discordjs/',
'',
)}, ${version}, ${url}) on conflict (name, version) do update set url = EXCLUDED.url`;
} catch (error) {
const err = error as Error;
setFailed(err.message);
console.log(err.message);
}
}

View File

@@ -1,7 +1,7 @@
import process from 'node:process';
import { setFailed } from '@actions/core';
import { generateAllIndices } from '@discordjs/scripts';
import { connect } from '@planetscale/database';
import { createPool } from '@vercel/postgres';
import { MeiliSearch } from 'meilisearch';
import { fetch } from 'undici';
@@ -17,9 +17,8 @@ if (!process.env.SEARCH_API_KEY) {
setFailed('SEARCH_API_KEY is not set');
}
const sql = connect({
fetch,
url: process.env.DATABASE_URL!,
const pool = createPool({
connectionString: process.env.DATABASE_URL,
});
const client = new MeiliSearch({
@@ -32,30 +31,32 @@ try {
const indices = await generateAllIndices({
fetchPackageVersions: async (pkg) => {
console.log(`Fetching versions for ${pkg}...`);
const { rows } = await sql.execute('select version from documentation where name = ?', [pkg]);
const { rows } = await pool.sql`select version from documentation where name = ${pkg}`;
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows.map((row) => row.version);
},
fetchPackageVersionDocs: async (pkg, version) => {
console.log(`Fetching data for ${pkg} ${version}...`);
const { rows } = await sql.execute('select data from documentation where name = ? and version = ?', [
pkg,
version,
]);
const { rows } = await pool.sql`select url from documentation where name = ${pkg} and version = ${version}`;
const res = await fetch(rows[0]?.url ?? '');
// @ts-expect-error: https://github.com/planetscale/database-js/issues/71
return rows[0].data;
return res.json();
},
writeToFile: false,
});
console.log('Generated all indices.');
console.log('Uploading indices...');
for (const index of indices) {
console.log(`Uploading ${index.index}...`);
await client.index(index.index).addDocuments(index.data);
}
try {
await Promise.all(
indices.map(async (index) => {
console.log(`Uploading ${index.index}...`);
await client.createIndex(index.index);
await client.index(index.index).addDocuments(index.data);
}),
);
} catch {}
console.log('Uploaded all indices.');
} catch (error) {

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -42,7 +42,7 @@
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0"
}

View File

@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { Buffer } from 'node:buffer';
import path from 'node:path';
import util from 'node:util';
import { TSDocConfiguration } from '@microsoft/tsdoc';
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
@@ -10,6 +13,7 @@ import {
PackageJsonLookup,
type IPackageJson,
type JsonObject,
FileSystem,
} from '@rushstack/node-core-library';
import { ApiDocumentedItem, type IApiDocumentedItemOptions } from '../items/ApiDocumentedItem.js';
import { ApiItem, ApiItemKind, type IApiItemJson } from '../items/ApiItem.js';
@@ -27,10 +31,58 @@ export interface IApiPackageOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiDocumentedItemOptions {
dependencies?: Record<string, string> | undefined;
projectFolderUrl?: string | undefined;
tsdocConfiguration: TSDocConfiguration;
}
const MinifyJSONMapping = {
canonicalReference: 'c',
constraintTokenRange: 'ctr',
dependencies: 'dp',
defaultTypeTokenRange: 'dtr',
docComment: 'd',
endIndex: 'en',
excerptTokens: 'ex',
extendsTokenRange: 'etr',
extendsTokenRanges: 'etrs',
fileColumn: 'co',
fileLine: 'l',
fileUrlPath: 'pat',
implementsTokenRanges: 'itrs',
initializerTokenRange: 'itr',
isAbstract: 'ab',
isOptional: 'op',
isProtected: 'pr',
isReadonly: 'ro',
isRest: 'rs',
isStatic: 'sta',
kind: 'k',
members: 'ms',
metadata: 'meta',
name: 'n',
oldestForwardsCompatibleVersion: 'ov',
overloadIndex: 'oi',
parameterName: 'pn',
parameterTypeTokenRange: 'ptr',
parameters: 'ps',
preserveMemberOrder: 'pmo',
projectFolderUrl: 'pdir',
propertyTypeTokenRange: 'prtr',
releaseTag: 'r',
returnTypeTokenRange: 'rtr',
schemaVersion: 'v',
startIndex: 'st',
text: 't',
toolPackage: 'tpk',
toolVersion: 'tv',
tsdocConfig: 'ts',
typeParameterName: 'tp',
typeParameters: 'tps',
typeTokenRange: 'ttr',
variableTypeTokenRange: 'vtr',
};
export interface IApiPackageMetadataJson {
/**
* To support forwards compatibility, the `oldestForwardsCompatibleVersion` field tracks the oldest schema version
@@ -75,10 +127,15 @@ export interface IApiPackageMetadataJson {
* Normally this configuration is loaded from the project's tsdoc.json file. It is stored
* in the .api.json file so that doc comments can be parsed accurately when loading the file.
*/
tsdocConfig: JsonObject;
tsdocConfig?: JsonObject;
}
export interface IApiPackageJson extends IApiItemJson {
/**
* Names of packages in the same monorepo this one uses mapped to the version of said package.
*/
dependencies?: Record<string, string>;
/**
* A file header that stores metadata about the tool that wrote the *.api.json file.
*/
@@ -98,6 +155,11 @@ export interface IApiPackageJson extends IApiItemJson {
* @public
*/
export interface IApiPackageSaveOptions extends IJsonFileSaveOptions {
/**
* Set to true to not have indentation or newlines in resulting JSON.
*/
minify?: boolean;
/**
* Set to true only when invoking API Extractor's test harness.
*
@@ -134,11 +196,31 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
private readonly _projectFolderUrl?: string | undefined;
private readonly _dependencies?: Record<string, string> | undefined;
public constructor(options: IApiPackageOptions) {
super(options);
this._tsdocConfiguration = options.tsdocConfiguration;
this._projectFolderUrl = options.projectFolderUrl;
if (options.dependencies) {
this._dependencies = options.dependencies;
} else {
const packageJson = PackageJsonLookup.instance.tryLoadPackageJsonFor('.');
if (packageJson?.dependencies) {
this._dependencies = {};
for (const [pack, semVer] of Object.entries(packageJson.dependencies)) {
const pathToPackage = path.join('..', pack.includes('/') ? pack.slice(pack.lastIndexOf('/')) : pack);
if (semVer === 'workspace:^') {
this._dependencies[pack] =
PackageJsonLookup.instance.tryLoadPackageJsonFor(pathToPackage)?.version ?? 'unknown';
} else if (FileSystem.exists(pathToPackage)) {
this._dependencies[pack] = semVer;
}
}
}
}
}
/**
@@ -152,11 +234,17 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
super.onDeserializeInto(options, context, jsonObject);
options.projectFolderUrl = jsonObject.projectFolderUrl;
options.dependencies = jsonObject.dependencies;
}
public static loadFromJsonFile(apiJsonFilename: string): ApiPackage {
const jsonObject: IApiPackageJson = JsonFile.load(apiJsonFilename);
return this.loadFromJson(JsonFile.load(apiJsonFilename), apiJsonFilename);
}
public static loadFromJson(rawJson: any, apiJsonFilename: string = ''): ApiPackage {
const jsonObject =
MinifyJSONMapping.metadata in rawJson ? this._mapFromMinified(rawJson) : (rawJson as IApiPackageJson);
if (!jsonObject?.metadata) throw new Error(util.inspect(rawJson, { depth: 2 }));
if (!jsonObject?.metadata || typeof jsonObject.metadata.schemaVersion !== 'number') {
throw new Error(
`Error loading ${apiJsonFilename}:` +
@@ -205,7 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004) {
if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfiguration' in jsonObject) {
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig);
if (tsdocConfigFile.hasErrors) {
throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary());
@@ -244,6 +332,10 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
return this.members as readonly ApiEntryPoint[];
}
public get dependencies(): Record<string, string> | undefined {
return this._dependencies;
}
/**
* The TSDoc configuration that was used when analyzing the API for this package.
*
@@ -299,8 +391,18 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
jsonObject.projectFolderUrl = this.projectFolderUrl;
}
if (this._dependencies) {
jsonObject.dependencies = this._dependencies;
}
this.serializeInto(jsonObject);
JsonFile.save(jsonObject, apiJsonFilename, ioptions);
if (ioptions.minify) {
FileSystem.writeFile(apiJsonFilename, Buffer.from(JSON.stringify(this._mapToMinified(jsonObject)), 'utf8'), {
ensureFolderExists: ioptions.ensureFolderExists ?? true,
});
} else {
JsonFile.save(jsonObject, apiJsonFilename, ioptions);
}
}
/**
@@ -309,4 +411,47 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
public override buildCanonicalReference(): DeclarationReference {
return DeclarationReference.package(this.name);
}
private _mapToMinified(jsonObject: IApiPackageJson) {
const mapper = (item: any): any => {
if (Array.isArray(item)) return item.map(mapper);
else {
const result: any = {};
for (const key of Object.keys(item)) {
if (key === 'dependencies') {
result[MinifyJSONMapping.dependencies] = item.dependencies;
} else
result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] =
typeof item[key] === 'object' ? mapper(item[key]) : item[key];
}
return result;
}
};
return mapper(jsonObject);
}
private static _mapFromMinified(jsonObject: any): IApiPackageJson {
const mapper = (item: any): any => {
if (Array.isArray(item)) return item.map(mapper);
else {
const result: any = {};
for (const key of Object.keys(item)) {
if (key === MinifyJSONMapping.dependencies) {
result.dependencies = item[MinifyJSONMapping.dependencies];
} else
result[
Object.keys(MinifyJSONMapping).find(
(look) => MinifyJSONMapping[look as keyof typeof MinifyJSONMapping] === key,
)!
] = typeof item[key] === 'object' ? mapper(item[key]) : item[key];
}
return result;
}
};
return mapper(jsonObject) as IApiPackageJson;
}
}

View File

@@ -96,13 +96,18 @@ export enum ApiJsonSchemaVersion {
*/
V_1012 = 1_012,
/**
* Make tsdocConfiguration optional
*/
V_1013 = 1_013,
/**
* The current latest .api.json schema version.
*
* IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE`
* should be updated.
*/
LATEST = V_1012,
LATEST = V_1013,
/**
* The oldest .api.json schema version that is still supported for backwards compatibility.
@@ -119,7 +124,7 @@ export enum ApiJsonSchemaVersion {
* if the older library would not be able to deserialize your new file format. Adding a nonessential field
* is generally okay. Removing, modifying, or reinterpreting existing fields is NOT safe.
*/
OLDEST_FORWARDS_COMPATIBLE = V_1001,
OLDEST_FORWARDS_COMPATIBLE = V_1013,
}
export class DeserializerContext {

View File

@@ -1,3 +1,5 @@
import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig();
export default createTsupConfig({
minify: 'terser',
});

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -54,7 +54,7 @@
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2"

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -5,12 +5,12 @@
"apiReport": {
"enabled": true,
"reportFolder": "../../../common/reviews/api"
"reportFolder": "<projectFolder>/docs/review"
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "../../../common/temp/api/<unscopedPackageName>.api.json"
"apiJsonFilePath": "<projectFolder>/docs/<unscopedPackageName>.api.json"
},
"dtsRollup": {

View File

@@ -74,7 +74,7 @@
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0"
}

View File

@@ -41,6 +41,11 @@ export interface IExtractorInvokeOptions {
*/
compilerState?: CompilerState;
/**
* Whether to minify the resulting doc model JSON, i.e. without any indentation or newlines.
*/
docModelMinify?: boolean;
/**
* Indicates that API Extractor is running as part of a local build, e.g. on developer's
* machine.
@@ -270,7 +275,7 @@ export class Extractor {
apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, {
toolPackage: Extractor.packageName,
toolVersion: Extractor.version,
minify: options?.docModelMinify ?? false,
newlineConversion: extractorConfig.newlineKind,
ensureFolderExists: true,
testMode: extractorConfig.testMode,

View File

@@ -27,6 +27,8 @@ export class RunAction extends CommandLineAction {
private readonly _typescriptCompilerFolder: CommandLineStringParameter;
private readonly _minify: CommandLineFlagParameter;
public constructor(_parser: ApiExtractorCommandLine) {
super({
actionName: 'run',
@@ -57,6 +59,12 @@ export class RunAction extends CommandLineAction {
description: 'Show additional informational messages in the output.',
});
this._minify = this.defineFlagParameter({
parameterLongName: '--minify',
parameterShortName: '-m',
description: 'Minify the resulting doc model JSON, i.e. without any indentation or newlines.',
});
this._diagnosticsParameter = this.defineFlagParameter({
parameterLongName: '--diagnostics',
description:
@@ -136,6 +144,7 @@ export class RunAction extends CommandLineAction {
const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
localBuild: this._localParameter.value,
docModelMinify: this._minify.value,
showVerboseMessages: this._verboseParameter.value,
showDiagnostics: this._diagnosticsParameter.value,
typescriptCompilerFolder,

View File

@@ -4,7 +4,7 @@
import { ReleaseTag } from '@discordjs/api-extractor-model';
import * as tsdoc from '@microsoft/tsdoc';
import { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import ts from 'typescript';
import { PackageDocComment } from '../aedoc/PackageDocComment.js';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';

View File

@@ -3,7 +3,18 @@
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import type {
IApiMethodOptions,
ApiItemContainerMixin,
IApiParameterOptions,
IExcerptTokenRange,
IExcerptTokenRangeWithTypeParameters,
IExcerptToken,
IApiTypeParameterOptions,
IApiPropertyOptions,
} from '@discordjs/api-extractor-model';
import {
ApiItemKind,
ApiModel,
ApiClass,
ApiPackage,
@@ -13,16 +24,11 @@ import {
ApiNamespace,
ApiInterface,
ApiPropertySignature,
type ApiItemContainerMixin,
ReleaseTag,
ApiProperty,
ApiMethodSignature,
type IApiParameterOptions,
ApiEnum,
ApiEnumMember,
type IExcerptTokenRange,
type IExcerptTokenRangeWithTypeParameters,
type IExcerptToken,
ApiConstructor,
ApiConstructSignature,
ApiFunction,
@@ -30,13 +36,11 @@ import {
ApiVariable,
ApiTypeAlias,
ApiCallSignature,
type IApiTypeParameterOptions,
EnumMemberOrder,
ExcerptTokenKind,
Navigation,
} from '@discordjs/api-extractor-model';
import type * as tsdoc from '@microsoft/tsdoc';
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';
@@ -208,24 +212,43 @@ interface IProcessAstEntityContext {
const linkRegEx = /{@link\s(?<class>\w+)#(?<event>event:)?(?<prop>[\w()]+)(?<name>\s[^}]*)?}/g;
function fixLinkTags(input?: string): string | undefined {
return input?.replaceAll(linkRegEx, '{@link $<class>.$<prop>$<name>}');
return input?.replaceAll(
linkRegEx,
(_match, _p1, _p2, _p3, _p4, _offset, _string, groups) =>
`{@link ${groups.class}.${groups.prop}${groups.name ? ` |${groups.name}` : ''}}`,
);
}
function filePathFromJson(meta: DocgenMetaJson): string {
return `${meta.path.slice('packages/discord.js/'.length)}/${meta.file}`;
}
function fixPrimitiveTypes(type: string, symbol: string | undefined) {
switch (type) {
case '*':
return 'any';
case 'Object':
return symbol === '<' ? 'Record' : 'object';
default:
return type;
}
}
export class ApiModelGenerator {
private readonly _collector: Collector;
private readonly _apiModel: ApiModel;
private readonly _tsDocParser: tsdoc.TSDocParser;
private readonly _referenceGenerator: DeclarationReferenceGenerator;
public constructor(collector: Collector) {
this._collector = collector;
this._apiModel = new ApiModel();
this._referenceGenerator = new DeclarationReferenceGenerator(collector);
// @ts-expect-error we reuse the private tsdocParser from collector here
this._tsDocParser = collector._tsdocParser;
}
public get apiModel(): ApiModel {
@@ -430,6 +453,42 @@ export class ApiModelGenerator {
name: childDeclaration.astSymbol.localName,
});
}
for (const method of (context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined)?.methods ??
[]) {
switch (context.parentApiItem.kind) {
case ApiItemKind.Class:
this._processApiMethod(null, { ...context, name: method.name });
break;
case ApiItemKind.Interface:
this._processApiMethodSignature(null, { ...context, name: method.name });
break;
default:
console.log(
`Found docgen method not in TS typings for ApiItem of kind ${ApiItemKind[context.parentApiItem.kind]}`,
);
}
}
for (const property of (context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined)?.props ??
[]) {
switch (context.parentApiItem.kind) {
case ApiItemKind.Class:
this._processApiProperty(null, { ...context, name: property.name });
break;
case ApiItemKind.Interface:
this._processApiPropertySignature(null, { ...context, name: property.name });
break;
default:
console.log(
`Found docgen property not in TS typings for ApiItem of kind ${ApiItemKind[context.parentApiItem.kind]}`,
);
}
}
}
private _processApiCallSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
@@ -500,7 +559,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = parent?.construct
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/*+\n * ${fixLinkTags(parent.construct.description)}\n${
parent.construct.params
?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`)
@@ -586,7 +645,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${
jsDoc.deprecated
? ` * @deprecated ${
@@ -658,7 +717,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = parent?.construct
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/*+\n * ${fixLinkTags(parent.construct.description)}\n${
parent.construct.params
?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`)
@@ -789,7 +848,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${
jsDoc.params?.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('') ??
''
@@ -916,7 +975,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/**\n * ${fixLinkTags(jsDoc.description)}\n${jsDoc.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${
jsDoc.deprecated
? ` * @deprecated ${
@@ -952,86 +1011,103 @@ export class ApiModelGenerator {
});
}
private _processApiMethod(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiMethod(astDeclaration: AstDeclaration | null, context: IProcessAstEntityContext): void {
const { name, parentApiItem } = context;
const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0;
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | undefined;
const jsDoc = parent?.methods?.find((method) => method.name === name);
const isStatic: boolean = astDeclaration
? (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0
: jsDoc?.scope === 'static';
const overloadIndex: number = astDeclaration ? this._collector.getOverloadIndex(astDeclaration) : 1;
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;
if (astDeclaration) {
const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: methodDeclaration.type, tokenRange: returnTypeTokenRange });
const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: methodDeclaration.type, tokenRange: returnTypeTokenRange });
const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters(
nodesToCapture,
methodDeclaration.typeParameters,
);
const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters(
nodesToCapture,
methodDeclaration.typeParameters,
);
const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodDeclaration.parameters);
const parameters: IApiParameterOptions[] = this._captureParameters(
nodesToCapture,
methodDeclaration.parameters,
);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
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"
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._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.examples?.map((example) => ` * @example\n * \`\`\`js\n * ${example}\n * \`\`\`\n`).join('') ?? ''
}${
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"
}
const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0;
const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0;
const sourceLocation: ISourceLocation = this._getSourceLocation(methodDeclaration);
apiMethod = new ApiMethod({
name,
isAbstract,
docComment,
releaseTag,
isProtected,
isStatic,
isOptional,
typeParameters,
parameters,
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
} else if (jsDoc) {
const methodOptions = this._mapMethod(jsDoc, parentApiItem.getAssociatedPackage()!.name);
if (methodOptions.releaseTag === ReleaseTag.Internal || methodOptions.releaseTag === ReleaseTag.Alpha) {
return; // trim out items marked as "@internal" or "@alpha"
}
apiMethod = new ApiMethod(methodOptions);
}
const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0;
const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0;
const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0;
const sourceLocation: ISourceLocation = this._getSourceLocation(methodDeclaration);
apiMethod = new ApiMethod({
name,
isAbstract,
docComment,
releaseTag,
isProtected,
isStatic,
isOptional,
typeParameters,
parameters,
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
parentApiItem.addMember(apiMethod);
}
}
private _processApiMethodSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiMethodSignature(astDeclaration: AstDeclaration | null, context: IProcessAstEntityContext): void {
const { name, parentApiItem } = context;
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
const overloadIndex: number = astDeclaration ? this._collector.getOverloadIndex(astDeclaration) : 1;
const containerKey: string = ApiMethodSignature.getContainerKey(name, overloadIndex);
let apiMethodSignature: ApiMethodSignature | undefined = parentApiItem.tryGetMemberByKey(
@@ -1041,58 +1117,63 @@ export class ApiModelGenerator {
const jsDoc = parent?.methods?.find((method) => method.name === name);
if (apiMethodSignature === undefined) {
const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature;
if (astDeclaration) {
const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: methodSignature.type, tokenRange: returnTypeTokenRange });
const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: methodSignature.type, tokenRange: returnTypeTokenRange });
const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters(
nodesToCapture,
methodSignature.typeParameters,
);
const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters(
nodesToCapture,
methodSignature.typeParameters,
);
const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodSignature.parameters);
const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodSignature.parameters);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
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);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._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);
apiMethodSignature = new ApiMethodSignature({
name,
docComment,
releaseTag,
isOptional,
typeParameters,
parameters,
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
apiMethodSignature = new ApiMethodSignature({
name,
docComment,
releaseTag,
isOptional,
typeParameters,
parameters,
overloadIndex,
excerptTokens,
returnTypeTokenRange,
fileUrlPath: jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc?.meta.line ?? sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
} else if (jsDoc) {
apiMethodSignature = new ApiMethodSignature(this._mapMethod(jsDoc, parentApiItem.getAssociatedPackage()!.name));
}
parentApiItem.addMember(apiMethodSignature);
}
@@ -1130,77 +1211,93 @@ export class ApiModelGenerator {
});
}
private _processApiProperty(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiProperty(astDeclaration: AstDeclaration | null, context: IProcessAstEntityContext): void {
const { name, parentApiItem } = context;
const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0;
const parent = context.parentDocgenJson as DocgenClassJson | DocgenInterfaceJson | DocgenTypedefJson | undefined;
const jsDoc = parent?.props?.find((prop) => prop.name === name);
const isStatic: boolean = astDeclaration
? (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0
: parentApiItem.kind === ApiItemKind.Class || parentApiItem.kind === ApiItemKind.Interface
? (jsDoc as DocgenPropertyJson).scope === 'static'
: false;
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;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
if (astDeclaration) {
const declaration: ts.Declaration = astDeclaration.declaration;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
let propertyTypeNode: ts.TypeNode | undefined;
const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
let propertyTypeNode: ts.TypeNode | undefined;
if (ts.isPropertyDeclaration(declaration) || ts.isGetAccessorDeclaration(declaration)) {
propertyTypeNode = declaration.type;
if (ts.isPropertyDeclaration(declaration) || ts.isGetAccessorDeclaration(declaration)) {
propertyTypeNode = declaration.type;
}
if (ts.isSetAccessorDeclaration(declaration)) {
// Note that TypeScript always reports an error if a setter does not have exactly one parameter.
propertyTypeNode = declaration.parameters[0]!.type;
}
nodesToCapture.push({ node: propertyTypeNode, tokenRange: propertyTypeTokenRange });
let initializerTokenRange: IExcerptTokenRange | undefined;
if (ts.isPropertyDeclaration(declaration) && declaration.initializer) {
initializerTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: declaration.initializer, tokenRange: initializerTokenRange });
}
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._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;
const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0;
const isReadonly: boolean = this._isReadonly(astDeclaration);
const sourceLocation: ISourceLocation = this._getSourceLocation(declaration);
apiProperty = new ApiProperty({
name,
docComment,
releaseTag,
isAbstract,
isProtected,
isStatic,
isOptional,
isReadonly,
excerptTokens,
propertyTypeTokenRange,
initializerTokenRange,
fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
} else if (parentApiItem.kind === ApiItemKind.Class || parentApiItem.kind === ApiItemKind.Interface) {
const propertyOptions = this._mapProp(jsDoc as DocgenPropertyJson, parentApiItem.getAssociatedPackage()!.name);
if (propertyOptions.releaseTag === ReleaseTag.Internal || propertyOptions.releaseTag === ReleaseTag.Alpha) {
return; // trim out items marked as "@internal" or "@alpha"
}
apiProperty = new ApiProperty(propertyOptions);
} else {
console.log(`We got a property in ApiItem of kind ${ApiItemKind[parentApiItem.kind]}`);
}
if (ts.isSetAccessorDeclaration(declaration)) {
// Note that TypeScript always reports an error if a setter does not have exactly one parameter.
propertyTypeNode = declaration.parameters[0]!.type;
}
nodesToCapture.push({ node: propertyTypeNode, tokenRange: propertyTypeTokenRange });
let initializerTokenRange: IExcerptTokenRange | undefined;
if (ts.isPropertyDeclaration(declaration) && declaration.initializer) {
initializerTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: declaration.initializer, tokenRange: initializerTokenRange });
}
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
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;
const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0;
const isReadonly: boolean = this._isReadonly(astDeclaration);
const sourceLocation: ISourceLocation = this._getSourceLocation(declaration);
apiProperty = new ApiProperty({
name,
docComment,
releaseTag,
isAbstract,
isProtected,
isStatic,
isOptional,
isReadonly,
excerptTokens,
propertyTypeTokenRange,
initializerTokenRange,
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);
} else {
// If the property was already declared before (via a merged interface declaration),
@@ -1208,7 +1305,7 @@ export class ApiModelGenerator {
}
}
private _processApiPropertySignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiPropertySignature(astDeclaration: AstDeclaration | null, context: IProcessAstEntityContext): void {
const { name, parentApiItem } = context;
const containerKey: string = ApiPropertySignature.getContainerKey(name);
@@ -1219,41 +1316,49 @@ export class ApiModelGenerator {
const jsDoc = parent?.props?.find((prop) => prop.name === name);
if (apiPropertySignature === undefined) {
const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature;
if (astDeclaration) {
const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature;
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: propertySignature.type, tokenRange: propertyTypeTokenRange });
const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange();
nodesToCapture.push({ node: propertySignature.type, tokenRange: propertyTypeTokenRange });
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
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);
const sourceLocation: ISourceLocation = this._getSourceLocation(propertySignature);
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? this._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);
const sourceLocation: ISourceLocation = this._getSourceLocation(propertySignature);
apiPropertySignature = new ApiPropertySignature({
name,
docComment,
releaseTag,
isOptional,
excerptTokens,
propertyTypeTokenRange,
isReadonly,
fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
apiPropertySignature = new ApiPropertySignature({
name,
docComment,
releaseTag,
isOptional,
excerptTokens,
propertyTypeTokenRange,
isReadonly,
fileUrlPath: jsDoc && 'meta' in jsDoc ? filePathFromJson(jsDoc.meta) : sourceLocation.sourceFilePath,
fileLine: jsDoc && 'meta' in jsDoc ? jsDoc.meta.line : sourceLocation.sourceFileLine,
fileColumn: sourceLocation.sourceFileColumn,
});
} else if (parentApiItem.kind === ApiItemKind.Class || parentApiItem.kind === ApiItemKind.Interface) {
apiPropertySignature = new ApiPropertySignature(
this._mapProp(jsDoc as DocgenPropertyJson, parentApiItem.getAssociatedPackage()!.name),
);
} else {
console.log(`We got a property in ApiItem of kind ${ApiItemKind[parentApiItem.kind]}`);
}
parentApiItem.addMember(apiPropertySignature);
} else {
@@ -1290,7 +1395,7 @@ export class ApiModelGenerator {
const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture);
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const docComment: tsdoc.DocComment | undefined = jsDoc
? new TSDocParser().parseString(
? this._tsDocParser.parseString(
`/**\n * ${fixLinkTags(jsDoc.description) ?? ''}\n${
'params' in jsDoc
? jsDoc.params.map((param) => ` * @param ${param.name} - ${fixLinkTags(param.description)}\n`).join('')
@@ -1425,7 +1530,7 @@ export class ApiModelGenerator {
});
}
const docComment: tsdoc.DocComment | undefined = new TSDocParser().parseString(
const docComment: tsdoc.DocComment | undefined = this._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('') : ''}${
@@ -1574,35 +1679,162 @@ export class ApiModelGenerator {
[ts.SyntaxKind.InterfaceDeclaration]: 'interface',
[ts.SyntaxKind.TypeAliasDeclaration]: 'type',
};
return mapper.flatMap((typ) =>
typ.reduce<IExcerptToken[]>(
(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 ?? '' },
],
[],
),
);
return mapper
.flatMap((typ, index) => {
const result = typ.reduce<IExcerptToken[]>(
(arr, [type, symbol]) => [
...arr,
{
kind: type?.includes("'") ? ExcerptTokenKind.Content : ExcerptTokenKind.Reference,
text: fixPrimitiveTypes(type ?? 'unknown', symbol),
canonicalReference: type?.includes("'")
? undefined
: 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 ?? '' },
],
[],
);
return index === 0 ? result : [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result];
})
.filter((excerpt) => excerpt.text.length);
}
private _mapProp(prop: DocgenPropertyJson, _package: string): IApiPropertyOptions {
const mappedVarType = this._mapVarType(prop.type);
return {
name: prop.name,
isAbstract: Boolean(prop.abstract),
isProtected: prop.access === 'protected',
isStatic: prop.scope === 'static',
isOptional: Boolean(prop.nullable),
isReadonly: Boolean(prop.readonly),
docComment: this._tsDocParser.parseString(
`/**\n * ${prop.description}\n${prop.see?.map((see) => ` * @see ${see}\n`).join('') ?? ''}${
prop.readonly ? ' * @readonly\n' : ''
} */`,
).docComment,
excerptTokens: [
{
kind: ExcerptTokenKind.Content,
text: `${prop.access} ${prop.scope === 'static' ? 'static ' : ''}${prop.readonly ? 'readonly ' : ''}${
prop.name
} :`,
},
...mappedVarType,
{ kind: ExcerptTokenKind.Content, text: ';' },
],
propertyTypeTokenRange: { startIndex: 1, endIndex: 1 + mappedVarType.length },
releaseTag: prop.access === 'private' ? ReleaseTag.Internal : ReleaseTag.Public,
fileLine: prop.meta?.line ?? 0,
fileUrlPath: prop.meta ? `${prop.meta.path.slice(`packages/${_package}/`.length)}/${prop.meta.file}` : '',
};
}
private _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),
},
};
}
private _mapMethod(method: DocgenMethodJson, _package: string): IApiMethodOptions {
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 = this._mapVarType(method.params![index]!.type);
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 = this._mapVarType(method.params[method.params.length - 1]!.type);
paramTokens.push(newTokens.length);
excerptTokens.push(...newTokens);
excerptTokens.push({ kind: ExcerptTokenKind.Content, text: `): ` });
}
const returnTokens = this._mapVarType(method.returns?.[0] ?? []);
excerptTokens.push(...returnTokens);
excerptTokens.push({ kind: ExcerptTokenKind.Content, text: ';' });
return {
name: method.name,
isAbstract: Boolean(method.abstract),
isOptional: false,
isProtected: method.access === 'protected',
isStatic: method.scope === 'static',
overloadIndex: 1,
parameters: method.params?.map((param, index) => this._mapParam(param, index, _package, paramTokens)) ?? [],
releaseTag: method.access === 'private' ? ReleaseTag.Internal : ReleaseTag.Public,
returnTypeTokenRange: method.returns?.length
? { startIndex: excerptTokens.length - 1 - returnTokens.length, endIndex: excerptTokens.length - 1 }
: { startIndex: 0, endIndex: 0 },
typeParameters: [],
docComment: this._tsDocParser.parseString(
`/**\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}`
: ''
}${method.examples?.map((example) => ` * @example\n * \`\`\`js\n * ${example}\n * \`\`\`\n`).join('') ?? ''}${
method.deprecated
? ` * @deprecated ${typeof method.deprecated === 'boolean' ? 'yes' : method.deprecated}\n`
: ''
} */`,
).docComment,
excerptTokens,
fileLine: method.meta.line,
fileUrlPath: `${method.meta.path.slice(`packages/${_package}/`.length)}/${method.meta.file}`,
};
}
}

View File

@@ -135,7 +135,7 @@ export class ExcerptBuilder {
return { startIndex: 0, endIndex: 0, typeParameters: [] };
}
public static isPrimitiveKeyword(node: ts.Node): boolean {
private static _isPrimitiveKeyword(node: ts.Node): boolean {
switch (node.kind) {
case ts.SyntaxKind.AnyKeyword:
case ts.SyntaxKind.BigIntKeyword:
@@ -156,6 +156,15 @@ export class ExcerptBuilder {
}
}
private static _isRedundantBarAfterColon(span: Span) {
return (
span.kind === ts.SyntaxKind.BarToken &&
span.previousSibling === undefined &&
(span.parent?.parent?.previousSibling?.kind === ts.SyntaxKind.LessThanToken ||
span.parent?.parent?.previousSibling?.kind === ts.SyntaxKind.ColonToken)
);
}
private static _buildSpan(excerptTokens: IExcerptToken[], span: Span, state: IBuildSpanState): boolean {
if (span.kind === ts.SyntaxKind.JSDocComment) {
// Discard any comments
@@ -174,21 +183,21 @@ export class ExcerptBuilder {
if (span.prefix) {
let canonicalReference: DeclarationReference | undefined;
if (span.kind === ts.SyntaxKind.Identifier) {
const name: ts.Identifier = span.node as ts.Identifier;
if (ts.isIdentifier(span.node)) {
const name: ts.Identifier = span.node;
canonicalReference = state.referenceGenerator.getDeclarationReferenceForIdentifier(name);
}
if (canonicalReference) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix, canonicalReference);
} else if (
ExcerptBuilder.isPrimitiveKeyword(span.node) ||
(span.node.kind === ts.SyntaxKind.Identifier &&
ExcerptBuilder._isPrimitiveKeyword(span.node) ||
(ts.isIdentifier(span.node) &&
((ts.isTypeReferenceNode(span.node.parent) && span.node.parent.typeName === span.node) ||
(ts.isTypeParameterDeclaration(span.node.parent) && span.node.parent.name === span.node)))
) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix);
} else {
} else if (!ExcerptBuilder._isRedundantBarAfterColon(span)) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.prefix);
}
@@ -313,6 +322,12 @@ export class ExcerptBuilder {
!startOrEndIndices.has(currentIndex)
) {
prevToken.text += currentToken.text;
// Remove BarTokens from excerpts if they immediately follow a LessThanToken, e.g. `Promise< | Something>`
// would become `Promise<Something>`
if (/<\s*\|/.test(prevToken.text)) {
prevToken.text = prevToken.text.replace(/<\s*\|\s*/, '<');
}
mergeCount = 1;
} else {
// Otherwise, no merging can occur here. Continue to the next index.

View File

@@ -2,6 +2,7 @@ import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig({
entry: ['src/**/*.ts'],
minify: 'terser',
cjsInterop: true,
noExternal: ['@microsoft/tsdoc*'],
});

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -10,7 +10,7 @@
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__",
"fmt": "pnpm run format",
"docs": "pnpm run build:docs && api-extractor run --local",
"docs": "pnpm run build:docs && api-extractor run --local --minify",
"prepack": "pnpm run lint && pnpm run test && pnpm run build",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/brokers/*'",
"release": "cliff-jumper"
@@ -71,15 +71,16 @@
"ioredis": "^5.3.2"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.2.1",
"@discordjs/api-extractor": "workspace:^",
"@favware/cliff-jumper": "^2.2.1",
"@types/node": "18.18.8",
"@vitest/coverage-v8": "^0.34.6",
"cross-env": "^7.0.3",
"esbuild-plugin-version-injector": "^1.2.1",
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -3,3 +3,11 @@ export * from './brokers/redis/PubSubRedis.js';
export * from './brokers/redis/RPCRedis.js';
export * from './brokers/Broker.js';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/brokers#readme | @discordjs/brokers} version
* that you are currently using.
*
* @privateRemarks This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild.
*/
export const version = '[VI]{{inject}}[/VI]' as string;

View File

@@ -1,3 +1,6 @@
import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig();
export default createTsupConfig({
esbuildPlugins: [esbuildPluginVersionInjector()],
});

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -10,7 +10,7 @@
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__",
"fmt": "pnpm run format",
"docs": "pnpm run build:docs && api-extractor run --local",
"docs": "pnpm run build:docs && api-extractor run --local --minify",
"prepack": "pnpm run lint && pnpm run test && pnpm run build",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'",
"release": "cliff-jumper"
@@ -73,8 +73,8 @@
"tslib": "^2.6.2"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.2.1",
"@discordjs/api-extractor": "workspace:^",
"@favware/cliff-jumper": "^2.2.1",
"@types/node": "16.18.60",
"@vitest/coverage-v8": "^0.34.6",
"cross-env": "^7.0.3",
@@ -83,7 +83,7 @@
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -10,7 +10,7 @@
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__",
"fmt": "pnpm run format",
"docs": "pnpm run build:docs && api-extractor run --local",
"docs": "pnpm run build:docs && api-extractor run --local --minify",
"prepack": "pnpm run lint && pnpm run test && pnpm run build",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'",
"release": "cliff-jumper"
@@ -60,8 +60,8 @@
},
"homepage": "https://discord.js.org",
"devDependencies": {
"@favware/cliff-jumper": "^2.2.1",
"@discordjs/api-extractor": "workspace:^",
"@favware/cliff-jumper": "^2.2.1",
"@types/node": "18.18.8",
"@vitest/coverage-v8": "^0.34.6",
"cross-env": "^7.0.3",
@@ -69,7 +69,7 @@
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -1,7 +1,7 @@
export * from './collection.js';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/collection/#readme | @discordjs/collection} version
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/collection#readme | @discordjs/collection} version
* that you are currently using.
*/
// This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild

View File

@@ -1 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

View File

@@ -1 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');

View File

@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
# [@discordjs/core@1.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.1.0...@discordjs/core@1.1.1) - (2023-11-17)
## Bug Fixes
- Minify mainlib docs json (#9963) ([4b88306](https://github.com/discordjs/discord.js/commit/4b88306dcb2b16b840ec61e9e33047af3a31c45d))
# [@discordjs/core@1.1.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.0.1...@discordjs/core@1.1.0) - (2023-11-12)
## Documentation

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/core",
"version": "1.1.0",
"version": "1.1.1",
"description": "A thinly abstracted wrapper around the rest API, and gateway.",
"scripts": {
"test": "vitest run",
@@ -9,7 +9,7 @@
"build:docs": "tsc -p tsconfig.docs.json",
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src",
"docs": "pnpm run build:docs && api-extractor run --local",
"docs": "pnpm run build:docs && api-extractor run --local --minify",
"prepack": "pnpm run build && pnpm run lint",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/core/*'",
"release": "cliff-jumper"
@@ -72,8 +72,8 @@
"discord-api-types": "0.37.61"
},
"devDependencies": {
"@favware/cliff-jumper": "^2.2.1",
"@discordjs/api-extractor": "workspace:^",
"@favware/cliff-jumper": "^2.2.1",
"@types/node": "18.18.8",
"@vitest/coverage-v8": "^0.34.6",
"cross-env": "^7.0.3",
@@ -81,7 +81,7 @@
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"tsup": "^7.2.0",
"turbo": "^1.10.17-canary.0",
"typescript": "^5.2.2",

View File

@@ -4,7 +4,7 @@ export * from '../util/index.js';
export * from 'discord-api-types/v10';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core/#readme | @discordjs/core} version
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core#readme | @discordjs/core} version
* that you are currently using.
*/
// This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild

View File

@@ -5,7 +5,7 @@ export * from './util/index.js';
export * from 'discord-api-types/v10';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core/#readme | @discordjs/core} version
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/core#readme | @discordjs/core} version
* that you are currently using.
*/
// This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild

View File

@@ -1,10 +1,14 @@
import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig({
entry: {
index: 'src/index.ts',
'http-only': 'src/http-only/index.ts',
},
esbuildPlugins: [esbuildPluginVersionInjector()],
});
export default [
createTsupConfig({
esbuildPlugins: [esbuildPluginVersionInjector()],
}),
createTsupConfig({
entry: {
'http-only': 'src/http-only/index.ts',
},
esbuildPlugins: [esbuildPluginVersionInjector()],
}),
];

View File

@@ -0,0 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');

Some files were not shown because too many files have changed in this diff Show More