feat(api-extractor): support multiple entrypoints (#10829)

* feat(api-extractor): support multiple entrypoints

* chore: initial support in generateSplitDocumentation

* chore: bring in line with upstream

* refactor: multiple entrypoints in scripts

* fix: split docs

* feat: website

* fix: docs failing on next

* fix: don't include dtypes for now

* refactor: don't fetch entrypoint if there is none

---------

Co-authored-by: iCrawl <buechler.noel@outlook.com>
This commit is contained in:
Qjuh
2025-05-12 23:48:41 +02:00
committed by GitHub
parent 4f5e5c7c14
commit b3db92edfb
93 changed files with 2330 additions and 1956 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ pids
# Dist
dist
dist-docs
packages/discord-api-types
# Miscellaneous
.tmp

View File

@@ -60,7 +60,7 @@
"lucide-react": "^0.503.0",
"meilisearch": "^0.49.0",
"motion": "^12.9.2",
"next": "15.4.0-canary.11",
"next": "15.4.0-canary.31",
"next-mdx-remote-client": "^2.1.1",
"next-themes": "^0.4.6",
"nuqs": "^2.4.3",

View File

@@ -4,6 +4,7 @@ import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted'
import type { Metadata } from 'next';
import Link from 'next/link';
import { Suspense, type PropsWithChildren } from 'react';
import { EntryPointSelect } from '@/components/EntrypointSelect';
import { Footer } from '@/components/Footer';
import { Navigation } from '@/components/Navigation';
import { Scrollbars } from '@/components/OverlayScrollbars';
@@ -13,14 +14,21 @@ import { ThemeSwitchNoSRR } from '@/components/ThemeSwitch';
import { VersionSelect } from '@/components/VersionSelect';
import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarTrigger } from '@/components/ui/Sidebar';
import { buttonStyles } from '@/styles/ui/button';
import { PACKAGES_WITH_ENTRY_POINTS } from '@/util/constants';
import { ENV } from '@/util/env';
import { fetchEntryPoints } from '@/util/fetchEntryPoints';
import { fetchVersions } from '@/util/fetchVersions';
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { CmdK } from './CmdK';
export async function generateMetadata({
params,
}: {
readonly params: Promise<{ readonly packageName: string; readonly version: string }>;
readonly params: Promise<{
readonly item?: string[] | undefined;
readonly packageName: string;
readonly version: string;
}>;
}): Promise<Metadata> {
const { packageName, version } = await params;
@@ -35,11 +43,22 @@ export async function generateMetadata({
export default async function Layout({
params,
children,
}: PropsWithChildren<{ readonly params: Promise<{ readonly packageName: string; readonly version: string }> }>) {
const { packageName, version } = await params;
}: PropsWithChildren<{
readonly params: Promise<{
readonly item?: string[] | undefined;
readonly packageName: string;
readonly version: string;
}>;
}>) {
const { packageName, version, item } = await params;
const versions = fetchVersions(packageName);
const hasEntryPoints = PACKAGES_WITH_ENTRY_POINTS.includes(packageName);
const entryPoints = hasEntryPoints ? fetchEntryPoints(packageName, version) : Promise.resolve([]);
const { entryPoints: parsedEntrypoints } = parseDocsPathParams(item);
return (
<>
<Sidebar closeButton={false} intent="inset">
@@ -65,12 +84,13 @@ export default async function Layout({
<PackageSelect />
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
<VersionSelect versionsPromise={versions} />
{hasEntryPoints ? <EntryPointSelect entryPointsPromise={entryPoints} /> : null}
<SearchButton />
</div>
</SidebarHeader>
<SidebarContent className="bg-[#f3f3f4] p-0 py-4 pl-4 dark:bg-[#121214]">
<Scrollbars>
<Navigation packageName={packageName} version={version} />
<Navigation entryPoint={parsedEntrypoints.join('.')} packageName={packageName} version={version} />
</Scrollbars>
</SidebarContent>
</Sidebar>

View File

@@ -0,0 +1,99 @@
'use cache';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import type { Metadata } from 'next';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import remarkGfm from 'remark-gfm';
import { DocItem } from '@/components/DocItem';
import { fetchNode } from '@/util/fetchNode';
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { getSingletonHighlighter } from '@/util/shiki.bundle';
export async function generateMetadata({
params,
}: {
readonly params: Promise<{
readonly item?: string[] | undefined;
readonly packageName: string;
readonly version: string;
}>;
}): Promise<Metadata> {
const { item, packageName, version } = await params;
const { foundItem } = parseDocsPathParams(item);
if (!foundItem) {
return {
title: `${packageName} (${version})`,
};
}
const decodedItemName = decodeURIComponent(foundItem);
const titlePart = decodedItemName.split(':')?.[0] ?? decodedItemName;
return {
title: `${titlePart} (${packageName} - ${version})`,
};
}
export default async function Page({
params,
}: {
readonly params: Promise<{
readonly item?: string[] | undefined;
readonly packageName: string;
readonly version: string;
}>;
}) {
const { item, packageName, version } = await params;
const { entryPoints: parsedEntrypoints, foundItem } = parseDocsPathParams(item);
if (!foundItem) {
const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
return (
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
<MDXRemote
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeShikiFromHighlighter,
await getSingletonHighlighter({
langs: ['typescript', 'javascript', 'shellscript'],
themes: ['github-light', 'github-dark-dimmed'],
}),
{
themes: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
},
],
],
},
}}
source={fileContent}
/>
</div>
);
}
const entryPointString = parsedEntrypoints.join('.');
const node = await fetchNode({
entryPoint: entryPointString,
item: decodeURIComponent(foundItem),
packageName,
version,
});
return (
<main className="mx-auto flex w-full max-w-screen-xl flex-col gap-8 px-6 py-4">
<DocItem node={node} packageName={packageName} version={version} />
</main>
);
}

View File

@@ -1,44 +0,0 @@
'use cache';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { DocItem } from '@/components/DocItem';
import { fetchNode } from '@/util/fetchNode';
export async function generateMetadata({
params,
}: {
readonly params: Promise<{
readonly item: string;
readonly packageName: string;
readonly version: string;
}>;
}): Promise<Metadata> {
const { item, packageName, version } = await params;
const normalizeItem = item.split(encodeURIComponent(':'))[0];
return {
title: `${normalizeItem} (${packageName} - ${version})`,
};
}
export default async function Page({
params,
}: {
readonly params: Promise<{ readonly item: string; readonly packageName: string; readonly version: string }>;
}) {
const { item, packageName, version } = await params;
const node = await fetchNode({ item, packageName, version });
if (!node) {
notFound();
}
return (
<main className="mx-auto flex w-full max-w-screen-xl flex-col gap-8 px-6 py-4">
<DocItem node={node} packageName={packageName} version={version} />
</main>
);
}

View File

@@ -1,42 +0,0 @@
'use cache';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import remarkGfm from 'remark-gfm';
import { getSingletonHighlighter } from '@/util/shiki.bundle';
export default async function Page({ params }: { readonly params: Promise<{ readonly packageName: string }> }) {
const { packageName } = await params;
const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
return (
<div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
<MDXRemote
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeShikiFromHighlighter,
await getSingletonHighlighter({
langs: ['typescript', 'javascript', 'shellscript'],
themes: ['github-light', 'github-dark-dimmed'],
}),
{
themes: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
},
],
],
},
}}
source={fileContent}
/>
</div>
);
}

View File

@@ -1,3 +1,5 @@
'use cache';
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ConstructorNode } from './ConstructorNode';
import { DeprecatedNode } from './DeprecatedNode';
@@ -26,8 +28,6 @@ async function OverloadNode({
readonly packageName: string;
readonly version: string;
}) {
'use cache';
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex flex-wrap gap-2">
@@ -63,8 +63,6 @@ export async function DocItem({
readonly packageName: string;
readonly version: string;
}) {
'use cache';
if (node.overloads?.length) {
return <OverloadNode node={node} packageName={packageName} version={version} />;
}

View File

@@ -0,0 +1,43 @@
'use client';
import { useParams, useRouter } from 'next/navigation';
import { use } from 'react';
import { parseDocsPathParams } from '@/util/parseDocsPathParams';
import { Select, SelectList, SelectOption, SelectTrigger } from './ui/Select';
export function EntryPointSelect({
entryPointsPromise,
}: {
readonly entryPointsPromise: Promise<{ readonly entryPoint: string }[]>;
}) {
const router = useRouter();
const params = useParams();
const entryPoints = use(entryPointsPromise);
const { entryPoints: parsedEntrypoints } = parseDocsPathParams(params.item as string[] | undefined);
return (
<Select
aria-label="Select an entrypoint"
defaultSelectedKey={parsedEntrypoints.length ? parsedEntrypoints.join('/') : 'global'}
>
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" />
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={entryPoints}>
{(item) => (
<SelectOption
className="dark:pressed:bg-[#313135] bg-[#f3f3f4] dark:bg-[#28282d] dark:hover:bg-[#313135]"
href={`/docs/packages/${params.packageName}/${params.version}/${item.entryPoint}`}
id={item.entryPoint || 'global'}
key={item.entryPoint || 'global'}
onHoverStart={() =>
router.prefetch(`/docs/packages/${params.packageName}/${params.version}/${item.entryPoint}`)
}
textValue={item.entryPoint || 'global'}
>
{item.entryPoint || 'global'}
</SelectOption>
)}
</SelectList>
</Select>
);
}

View File

@@ -5,8 +5,16 @@ import { resolveNodeKind } from './DocKind';
import { NavigationItem } from './NavigationItem';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
export async function Navigation({ packageName, version }: { readonly packageName: string; readonly version: string }) {
const node = await fetchSitemap({ packageName, version });
export async function Navigation({
entryPoint,
packageName,
version,
}: {
readonly entryPoint?: string | undefined;
readonly packageName: string;
readonly version: string;
}) {
const node = await fetchSitemap({ entryPoint, packageName, version });
if (!node) {
notFound();

View File

@@ -11,7 +11,10 @@ export const PACKAGES = [
{ name: 'util' },
{ name: 'voice' },
{ name: 'ws' },
// { name: 'discord-api-types' },
];
export const PACKAGES_WITH_ENTRY_POINTS = ['discord-api-types'];
export const DESCRIPTION =
"discord.js is a powerful Node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.";

View File

@@ -0,0 +1,34 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
// import { sql } from '@vercel/postgres';
import { ENV } from './env';
export async function fetchEntryPoints(packageName: string, version: string) {
if (ENV.IS_LOCAL_DEV) {
const fileContent = await readFile(
join(process.cwd(), `../../packages/${packageName}/docs/${packageName}/split/${version}.entrypoints.api.json`),
'utf8',
);
return JSON.parse(fileContent);
}
// try {
// const { rows } = await sql<{
// entryPoint: string;
// }>`select entryPoint from documentation where name = ${packageName} and version = ${version} order by
// case
// when version = 'main' then 0
// else 1
// end,
// case
// when version = 'main' then null
// else string_to_array(version, '.')::int[]
// end desc;
// `;
// return rows;
// } catch {
return [];
// }
}

View File

@@ -3,21 +3,24 @@ import { join } from 'node:path';
import { ENV } from './env';
export async function fetchNode({
entryPoint,
item,
packageName,
version,
}: {
readonly entryPoint?: string | undefined;
readonly item: any;
readonly packageName: string;
readonly version: string;
}) {
const normalizeItem = item.split(encodeURIComponent(':')).join('.').toLowerCase();
const normalizedEntryPoint = entryPoint ? `${entryPoint}.` : '';
const normalizeItem = item.replaceAll(':', '.').toLowerCase();
if (ENV.IS_LOCAL_DEV) {
const fileContent = await readFile(
join(
process.cwd(),
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizeItem}.api.json`,
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizedEntryPoint}${normalizeItem}.api.json`,
),
'utf8',
);
@@ -27,7 +30,7 @@ export async function fetchNode({
const isMain = version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizeItem}.api.json`,
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizedEntryPoint}${normalizeItem}.api.json`,
{ next: { revalidate: isMain ? 0 : 604_800 } },
);

View File

@@ -3,15 +3,22 @@ import { join } from 'node:path';
import { ENV } from './env';
export async function fetchSitemap({
entryPoint,
packageName,
version,
}: {
readonly entryPoint?: string | undefined;
readonly packageName: string;
readonly version: string;
}) {
const normalizedEntryPoint = entryPoint ? `${entryPoint}.` : '';
if (ENV.IS_LOCAL_DEV) {
const fileContent = await readFile(
join(process.cwd(), `../../packages/${packageName}/docs/${packageName}/split/${version}.sitemap.api.json`),
join(
process.cwd(),
`../../packages/${packageName}/docs/${packageName}/split/${version}.${normalizedEntryPoint}sitemap.api.json`,
),
'utf8',
);
@@ -20,7 +27,7 @@ export async function fetchSitemap({
const isMain = version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`,
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizedEntryPoint}sitemap.api.json`,
{
next: { revalidate: isMain ? 0 : 604_800 },
},

View File

@@ -0,0 +1,16 @@
export function parseDocsPathParams(item: string[] | undefined): {
entryPoints: string[];
foundItem: string | undefined;
} {
if (!item?.length) {
return { entryPoints: [], foundItem: undefined };
}
const lastElement = item.at(-1);
const hasTypeMarker = lastElement?.includes('%3A');
return {
entryPoints: hasTypeMarker ? item.slice(0, -1) : item,
foundItem: hasTypeMarker ? lastElement : undefined,
};
}

View File

@@ -1,11 +0,0 @@
{
"extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json",
// Enable code coverage for Jest
"collectCoverage": true,
"coverageDirectory": "<rootDir>/coverage",
"coverageReporters": ["cobertura", "html"],
// Use v8 coverage provider to avoid Babel
"coverageProvider": "v8"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@discordjs/api-extractor-model",
"version": "7.28.2",
"version": "7.30.6",
"description": "A helper library for loading and saving the .api.json files created by API Extractor",
"private": true,
"repository": {
@@ -31,19 +31,17 @@
}
},
"dependencies": {
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@rushstack/node-core-library": "4.1.0"
"@microsoft/tsdoc": "~0.15.1",
"@microsoft/tsdoc-config": "~0.17.1",
"@rushstack/node-core-library": "5.13.1"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.15.2",
"cross-env": "^7.0.3",
"eslint": "^9.25.1",
"eslint-config-neon": "^0.2.7",
"eslint-formatter-compact": "^8.40.0",
"eslint-formatter-pretty": "^6.0.1",
"jest": "^29.7.0",
"prettier": "^3.5.3",
"terser": "^5.39.0",
"tsup": "^8.4.0",

View File

@@ -6,6 +6,7 @@ import { InternalError } from '@rushstack/node-core-library';
import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js';
import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import type { Constructor, PropertiesOf } from '../mixins/Mixin.js';
import type { ApiEntryPoint } from '../model/ApiEntryPoint.js';
import type { ApiModel } from '../model/ApiModel.js';
import type { ApiPackage } from '../model/ApiPackage.js';
import type { DocgenJson } from '../model/Deserializer';
@@ -301,6 +302,20 @@ export class ApiItem {
return reversedParts.reverse().join('');
}
/**
* If this item is an ApiEntryPoint or has an ApiEntryPoint as one of its parents, then that object is returned.
* Otherwise undefined is returned.
*/
public getAssociatedEntryPoint(): ApiEntryPoint | undefined {
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (current.kind === ApiItemKind.EntryPoint) {
return current as ApiEntryPoint;
}
}
return undefined;
}
/**
* If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned.
* Otherwise undefined is returned.

View File

@@ -73,7 +73,8 @@ export class ModelReferenceResolver {
}
}
const importPath: string = declarationReference.importPath ?? '';
// Remove the leading / in the import path
const importPath: string = (declarationReference.importPath || '').replace(/^\//, '');
const foundEntryPoints: readonly ApiEntryPoint[] = apiPackage.findEntryPointsByPath(importPath);
if (foundEntryPoints.length < 1) {

View File

@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"types": ["node"],
"isolatedModules": false
},
"include": ["src/**/*.ts"],

View File

@@ -47,7 +47,7 @@
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@discordjs/api-extractor-model": "workspace:^",
"@microsoft/tsdoc": "0.14.2"
"@microsoft/tsdoc": "~0.15.1"
},
"devDependencies": {
"@types/node": "^22.15.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@discordjs/api-extractor",
"version": "7.38.1",
"version": "7.52.7",
"description": "Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups",
"private": true,
"keywords": [
@@ -51,20 +51,18 @@
},
"dependencies": {
"@discordjs/api-extractor-model": "workspace:^",
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@rushstack/node-core-library": "4.1.0",
"@rushstack/rig-package": "0.5.3",
"@rushstack/ts-command-line": "4.17.1",
"@microsoft/tsdoc": "~0.15.1",
"@microsoft/tsdoc-config": "~0.17.1",
"@rushstack/node-core-library": "5.13.1",
"@rushstack/ts-command-line": "5.0.1",
"colors": "~1.4.0",
"lodash": "~4.17.21",
"resolve": "~1.22.10",
"semver": "~7.6.3",
"source-map": "0.6.1",
"source-map": "~0.6.1",
"typescript": "~5.5.4"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.16",
"@types/node": "^22.15.2",
"@types/resolve": "^1.20.6",
@@ -75,7 +73,6 @@
"eslint-config-neon": "^0.2.7",
"eslint-formatter-compact": "^8.40.0",
"eslint-formatter-pretty": "^6.0.1",
"jest": "^29.7.0",
"prettier": "^3.5.3",
"terser": "^5.39.0",
"tsup": "^8.4.0",

View File

@@ -8,10 +8,10 @@ import type { AstSymbol } from './AstSymbol.js';
/**
* Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo}
*/
export class AstModuleExportInfo {
public readonly exportedLocalEntities: Map<string, AstEntity> = new Map<string, AstEntity>();
public readonly starExportedExternalModules: Set<AstModule> = new Set<AstModule>();
export interface IAstModuleExportInfo {
readonly exportedLocalEntities: Map<string, AstEntity>;
readonly starExportedExternalModules: Set<AstModule>;
readonly visitedAstModules: Set<AstModule>;
}
/**
@@ -64,7 +64,7 @@ export class AstModule {
/**
* Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`.
*/
public astModuleExportInfo: AstModuleExportInfo | undefined;
public astModuleExportInfo: IAstModuleExportInfo | undefined;
public constructor(options: IAstModuleOptions) {
this.sourceFile = options.sourceFile;

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { AstNamespaceImport, type IAstNamespaceImportOptions } from './AstNamespaceImport';
export interface IAstNamespaceExportOptions extends IAstNamespaceImportOptions {}
/**
* `AstNamespaceExport` represents a namespace that is created implicitly and exported by a statement
* such as `export * as example from "./file";`
*
* @remarks
*
* A typical input looks like this:
* ```ts
* // Suppose that example.ts exports two functions f1() and f2().
* export * as example from "./file";
* ```
*
* API Extractor's .d.ts rollup will transform it into an explicit namespace, like this:
* ```ts
* declare f1(): void;
* declare f2(): void;
*
* export declare namespace example {
* export {
* f1,
* f2
* }
* }
* ```
*
* The current implementation does not attempt to relocate f1()/f2() to be inside the `namespace`
* because other type signatures may reference them directly (without using the namespace qualifier).
* The AstNamespaceExports behaves the same as AstNamespaceImport, it just also has the inline export for the craeted namespace.
*/
export class AstNamespaceExport extends AstNamespaceImport {
public constructor(options: IAstNamespaceExportOptions) {
super(options);
}
}

View File

@@ -4,7 +4,7 @@
import type * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import { AstSyntheticEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
import type { AstModule, IAstModuleExportInfo } from './AstModule.js';
export interface IAstNamespaceImportOptions {
readonly astModule: AstModule;
@@ -88,8 +88,8 @@ export class AstNamespaceImport extends AstSyntheticEntity {
return this.namespaceName;
}
public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule);
public fetchAstModuleExportInfo(collector: Collector): IAstModuleExportInfo {
const astModuleExportInfo: IAstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule);
return astModuleExportInfo;
}
}

View File

@@ -5,7 +5,7 @@ import * as tsdoc from '@microsoft/tsdoc';
import * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
import type { WorkingPackage } from '../collector/WorkingPackage.js';
import type { IWorkingPackageEntryPoint, WorkingPackage } from '../collector/WorkingPackage.js';
import type { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule } from './AstModule.js';
@@ -52,7 +52,10 @@ export class AstReferenceResolver {
this._workingPackage = collector.workingPackage;
}
public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure {
public resolve(
declarationReference: tsdoc.DocDeclarationReference,
entryPoint: IWorkingPackageEntryPoint,
): AstDeclaration | ResolverFailure {
// Is it referring to the working package?
if (
declarationReference.packageName !== undefined &&
@@ -66,9 +69,7 @@ export class AstReferenceResolver {
return new ResolverFailure('Import paths are not supported');
}
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(
this._workingPackage.entryPointSourceFile,
);
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(entryPoint.sourceFile);
if (declarationReference.memberReferences.length === 0) {
return new ResolverFailure('Package references are not supported');

View File

@@ -8,7 +8,7 @@ import * as ts from 'typescript';
import type { MessageRouter } from '../collector/MessageRouter';
import { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
import type { AstModule, IAstModuleExportInfo } from './AstModule.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import { ExportAnalyzer } from './ExportAnalyzer.js';
@@ -126,7 +126,7 @@ export class AstSymbolTable {
/**
* This crawls the specified entry point and collects the full set of exported AstSymbols.
*/
public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(astModule: AstModule): IAstModuleExportInfo {
return this._exportAnalyzer.fetchAstModuleExportInfo(astModule);
}

View File

@@ -5,7 +5,8 @@ import { InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { AstEntity } from './AstEntity.js';
import { AstImport, type IAstImportOptions, AstImportKind } from './AstImport.js';
import { AstModule, AstModuleExportInfo } from './AstModule.js';
import { AstModule, type IAstModuleExportInfo } from './AstModule.js';
import { AstNamespaceExport } from './AstNamespaceExport.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import type { IFetchAstSymbolOptions } from './AstSymbolTable.js';
@@ -226,15 +227,19 @@ export class ExportAnalyzer {
/**
* Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}.
*/
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo {
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): IAstModuleExportInfo {
if (entryPointAstModule.isExternal) {
throw new Error('fetchAstModuleExportInfo() is not supported for external modules');
}
if (entryPointAstModule.astModuleExportInfo === undefined) {
const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo();
const astModuleExportInfo: IAstModuleExportInfo = {
visitedAstModules: new Set<AstModule>(),
exportedLocalEntities: new Map<string, AstEntity>(),
starExportedExternalModules: new Set<AstModule>(),
};
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set<AstModule>());
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule);
entryPointAstModule.astModuleExportInfo = astModuleExportInfo;
}
@@ -255,11 +260,7 @@ export class ExportAnalyzer {
: importOrExportDeclaration.moduleSpecifier;
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
specifier && ts.isStringLiteralLike(specifier)
? ts.getModeForUsageLocation(
importOrExportDeclaration.getSourceFile(),
specifier,
this._program.getCompilerOptions(),
)
? this._program.getModeForUsageLocation(importOrExportDeclaration.getSourceFile(), specifier)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
@@ -304,11 +305,8 @@ export class ExportAnalyzer {
return this._importableAmbientSourceFiles.has(sourceFile);
}
private _collectAllExportsRecursive(
astModuleExportInfo: AstModuleExportInfo,
astModule: AstModule,
visitedAstModules: Set<AstModule>,
): void {
private _collectAllExportsRecursive(astModuleExportInfo: IAstModuleExportInfo, astModule: AstModule): void {
const { visitedAstModules, starExportedExternalModules, exportedLocalEntities } = astModuleExportInfo;
if (visitedAstModules.has(astModule)) {
return;
}
@@ -316,7 +314,7 @@ export class ExportAnalyzer {
visitedAstModules.add(astModule);
if (astModule.isExternal) {
astModuleExportInfo.starExportedExternalModules.add(astModule);
starExportedExternalModules.add(astModule);
} else {
// Fetch each of the explicit exports for this module
if (astModule.moduleSymbol.exports) {
@@ -329,7 +327,7 @@ export class ExportAnalyzer {
// Don't collect the "export default" symbol unless this is the entry point module
if (
(exportName !== ts.InternalSymbolName.Default || visitedAstModules.size === 1) &&
!astModuleExportInfo.exportedLocalEntities.has(exportSymbol.name)
!exportedLocalEntities.has(exportSymbol.name)
) {
const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule);
@@ -341,7 +339,7 @@ export class ExportAnalyzer {
this._astSymbolTable.analyze(astEntity);
}
astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity);
exportedLocalEntities.set(exportSymbol.name, astEntity);
}
break;
@@ -350,7 +348,7 @@ export class ExportAnalyzer {
}
for (const starExportedModule of astModule.starExportedModules) {
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules);
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule);
}
}
}
@@ -550,8 +548,8 @@ export class ExportAnalyzer {
// SemicolonToken: pre=[;]
// Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780
const namespaceExport: ts.NamespaceExport = declaration as ts.NamespaceExport;
exportName = namespaceExport.name.getText().trim();
const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
return this._getAstNamespaceExport(astModule, declarationSymbol, declaration);
// throw new Error(
// `The "export * as ___" syntax is not supported yet; as a workaround,` +
// ` use "import * as ___" with a separate "export { ___ }" declaration\n` +
@@ -568,32 +566,32 @@ export class ExportAnalyzer {
if (exportDeclaration.moduleSpecifier) {
const externalModulePath: string | undefined = this._tryGetExternalModulePath(exportDeclaration);
if (declaration.kind === ts.SyntaxKind.NamespaceExport) {
if (externalModulePath === undefined) {
const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
// if (declaration.kind === ts.SyntaxKind.NamespaceExport) {
// if (externalModulePath === undefined) {
// const astModule: AstModule = this._fetchSpecifierAstModule(exportDeclaration, declarationSymbol);
// let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
// if (namespaceImport === undefined) {
// namespaceImport = new AstNamespaceImport({
// namespaceName: declarationSymbol.name,
// astModule,
// declaration,
// symbol: declarationSymbol,
// });
// this._astNamespaceImportByModule.set(astModule, namespaceImport);
// }
return namespaceImport;
}
// return namespaceImport;
// }
// Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
// a package or source file.
return this._fetchAstImport(undefined, {
importKind: AstImportKind.StarImport,
exportName,
modulePath: externalModulePath,
isTypeOnly: exportDeclaration.isTypeOnly,
});
}
// // Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
// // a package or source file.
// return this._fetchAstImport(undefined, {
// importKind: AstImportKind.StarImport,
// exportName,
// modulePath: externalModulePath,
// isTypeOnly: exportDeclaration.isTypeOnly,
// });
// }
if (externalModulePath !== undefined) {
return this._fetchAstImport(declarationSymbol, {
@@ -611,6 +609,21 @@ export class ExportAnalyzer {
return undefined;
}
private _getAstNamespaceExport(
astModule: AstModule,
declarationSymbol: ts.Symbol,
declaration: ts.Declaration,
): AstNamespaceExport {
const imoprtNamespace: AstNamespaceImport = this._getAstNamespaceImport(astModule, declarationSymbol, declaration);
return new AstNamespaceExport({
namespaceName: imoprtNamespace.localName,
astModule,
declaration,
symbol: declarationSymbol,
});
}
private _tryMatchImportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined {
const importDeclaration: ts.ImportDeclaration | undefined = TypeScriptHelpers.findFirstParent<ts.ImportDeclaration>(
declaration,
@@ -637,18 +650,7 @@ export class ExportAnalyzer {
if (externalModulePath === undefined) {
const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol);
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
return namespaceImport;
return this._getAstNamespaceImport(astModule, declarationSymbol, declaration);
}
// Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
@@ -770,6 +772,25 @@ export class ExportAnalyzer {
return undefined;
}
private _getAstNamespaceImport(
astModule: AstModule,
declarationSymbol: ts.Symbol,
declaration: ts.Declaration,
): AstNamespaceImport {
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
return namespaceImport;
}
private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean {
if (importDeclaration.importClause) {
return Boolean(importDeclaration.importClause.isTypeOnly);
@@ -883,10 +904,9 @@ export class ExportAnalyzer {
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
importOrExportDeclaration.moduleSpecifier && ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier)
? ts.getModeForUsageLocation(
? this._program.getModeForUsageLocation(
importOrExportDeclaration.getSourceFile(),
importOrExportDeclaration.moduleSpecifier,
this._program.getCompilerOptions(),
)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(

View File

@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
/* eslint-disable sonarjs/no-nested-switch */
import path from 'node:path';
import {
type PackageJsonLookup,
FileSystem,
@@ -9,7 +9,9 @@ import {
type NewlineKind,
type INodePackageJson,
type JsonObject,
type IPackageJsonExports,
} from '@rushstack/node-core-library';
import semver from 'semver';
import { ConsoleMessageId } from '../api/ConsoleMessageId.js';
import { Extractor } from '../api/Extractor.js';
import type { MessageRouter } from '../collector/MessageRouter.js';
@@ -43,6 +45,136 @@ export class PackageMetadata {
}
}
const TSDOC_METADATA_FILENAME = 'tsdoc-metadata.json' as const;
/**
* 1. If package.json a `"tsdocMetadata": "./path1/path2/tsdoc-metadata.json"` field
* then that takes precedence. This convention will be rarely needed, since the other rules below generally
* produce a good result.
*/
function _tryResolveTsdocMetadataFromTsdocMetadataField({ tsdocMetadata }: INodePackageJson): string | undefined {
return tsdocMetadata;
}
/**
* 2. If package.json contains a `"exports": { ".": { "types": "./path1/path2/index.d.ts" } }` field,
* then we look for the file under "./path1/path2/tsdoc-metadata.json"
*
* This always looks for a "." and then a "*" entry in the exports field, and then evaluates for
* a "types" field in that entry.
*/
function _tryResolveTsdocMetadataFromExportsField({ exports }: INodePackageJson): string | undefined {
switch (typeof exports) {
case 'string': {
return `${path.dirname(exports)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
if (Array.isArray(exports)) {
const [firstExport] = exports;
// Take the first entry in the array
if (firstExport) {
return `${path.dirname(firstExport)}/${TSDOC_METADATA_FILENAME}`;
}
} else {
const rootExport: IPackageJsonExports | string | null | undefined = exports['.'] ?? exports['*'];
switch (typeof rootExport) {
case 'string': {
return `${path.dirname(rootExport)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
let typesExport: IPackageJsonExports | string | undefined = rootExport?.types;
while (typesExport) {
switch (typeof typesExport) {
case 'string': {
return `${path.dirname(typesExport)}/${TSDOC_METADATA_FILENAME}`;
}
case 'object': {
typesExport = typesExport?.types;
break;
}
}
}
}
}
}
break;
}
}
return undefined;
}
/**
* 3. If package.json contains a `typesVersions` field, look for the version
* matching the highest minimum version that either includes a "." or "*" entry.
*/
function _tryResolveTsdocMetadataFromTypesVersionsField({ typesVersions }: INodePackageJson): string | undefined {
if (typesVersions) {
let highestMinimumMatchingSemver: semver.SemVer | undefined;
let latestMatchingPath: string | undefined;
for (const [version, paths] of Object.entries(typesVersions)) {
let range: semver.Range;
try {
range = new semver.Range(version);
} catch {
continue;
}
const minimumMatchingSemver: semver.SemVer | null = semver.minVersion(range);
if (
minimumMatchingSemver &&
(!highestMinimumMatchingSemver || semver.gt(minimumMatchingSemver, highestMinimumMatchingSemver))
) {
const pathEntry: string[] | undefined = paths['.'] ?? paths['*'];
const firstPath: string | undefined = pathEntry?.[0];
if (firstPath) {
highestMinimumMatchingSemver = minimumMatchingSemver;
latestMatchingPath = firstPath;
}
}
}
if (latestMatchingPath) {
return `${path.dirname(latestMatchingPath)}/${TSDOC_METADATA_FILENAME}`;
}
}
return undefined;
}
/**
* 4. If package.json contains a `"types": "./path1/path2/index.d.ts"` or a `"typings": "./path1/path2/index.d.ts"`
* field, then we look for the file under "./path1/path2/tsdoc-metadata.json".
*
* @remarks
* `types` takes precedence over `typings`.
*/
function _tryResolveTsdocMetadataFromTypesOrTypingsFields({ typings, types }: INodePackageJson): string | undefined {
const typesField: string | undefined = types ?? typings;
if (typesField) {
return `${path.dirname(typesField)}/${TSDOC_METADATA_FILENAME}`;
}
return undefined;
}
/**
* 5. If package.json contains a `"main": "./path1/path2/index.js"` field, then we look for the file under
* "./path1/path2/tsdoc-metadata.json".
*/
function _tryResolveTsdocMetadataFromMainField({ main }: INodePackageJson): string | undefined {
if (main) {
return `${path.dirname(main)}/${TSDOC_METADATA_FILENAME}`;
}
return undefined;
}
/**
* This class maintains a cache of analyzed information obtained from package.json
* files. It is built on top of the PackageJsonLookup class.
@@ -57,7 +189,7 @@ export class PackageMetadata {
* Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager.
*/
export class PackageMetadataManager {
public static tsdocMetadataFilename: string = 'tsdoc-metadata.json';
public static tsdocMetadataFilename: string = TSDOC_METADATA_FILENAME;
private readonly _packageJsonLookup: PackageJsonLookup;
@@ -70,37 +202,30 @@ export class PackageMetadataManager {
this._messageRouter = messageRouter;
}
// This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
/**
* This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
* In the future we will use the \@microsoft/tsdoc library to read this file.
*/
private static _resolveTsdocMetadataPathFromPackageJson(
packageFolder: string,
packageJson: INodePackageJson,
): string {
const tsdocMetadataFilename: string = PackageMetadataManager.tsdocMetadataFilename;
let tsdocMetadataRelativePath: string;
if (packageJson.tsdocMetadata) {
// 1. If package.json contains a field such as "tsdocMetadata": "./path1/path2/tsdoc-metadata.json",
// then that takes precedence. This convention will be rarely needed, since the other rules below generally
// produce a good result.
tsdocMetadataRelativePath = packageJson.tsdocMetadata;
} else if (packageJson.typings) {
// 2. If package.json contains a field such as "typings": "./path1/path2/index.d.ts", then we look
// for the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.typings), tsdocMetadataFilename);
} else if (packageJson.main) {
// 3. If package.json contains a field such as "main": "./path1/path2/index.js", then we look for
// the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.main), tsdocMetadataFilename);
} else {
// 4. If none of the above rules apply, then by default we look for the file under "./tsdoc-metadata.json"
// since the default entry point is "./index.js"
tsdocMetadataRelativePath = tsdocMetadataFilename;
}
const tsdocMetadataRelativePath: string =
_tryResolveTsdocMetadataFromTsdocMetadataField(packageJson) ??
_tryResolveTsdocMetadataFromExportsField(packageJson) ??
_tryResolveTsdocMetadataFromTypesVersionsField(packageJson) ??
_tryResolveTsdocMetadataFromTypesOrTypingsFields(packageJson) ??
_tryResolveTsdocMetadataFromMainField(packageJson) ??
// As a final fallback, place the file in the root of the package.
TSDOC_METADATA_FILENAME;
// Always resolve relative to the package folder.
const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath);
const tsdocMetadataPath: string = path.resolve(
packageFolder,
// This non-null assertion is safe because the last entry in TSDOC_METADATA_RESOLUTION_FUNCTIONS
// returns a non-undefined value.
tsdocMetadataRelativePath!,
);
return tsdocMetadataPath;
}

View File

@@ -2,7 +2,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError, Sort } from '@rushstack/node-core-library';
import { InternalError, Sort, Text } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import { IndentedWriter } from '../generators/IndentedWriter.js';
@@ -664,13 +664,7 @@ export class Span {
}
private _getTrimmed(text: string): string {
const trimmed: string = text.replaceAll(/\r?\n/g, '\\n');
if (trimmed.length > 100) {
return trimmed.slice(0, 97) + '...';
}
return trimmed;
return Text.truncateWithEllipsis(Text.convertToLf(text), 100);
}
private _getSubstring(startIndex: number, endIndex: number): string {

View File

@@ -12,12 +12,6 @@ export interface IGlobalVariableAnalyzer {
}
export class TypeScriptInternals {
public static getImmediateAliasedSymbol(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol {
// Compiler internal:
// https://github.com/microsoft/TypeScript/blob/v3.2.2/src/compiler/checker.ts
return (typeChecker as any).getImmediateAliasedSymbol(symbol);
}
/**
* Returns the Symbol for the provided Declaration. This is a workaround for a missing
* feature of the TypeScript Compiler API. It is the only apparent way to reach
@@ -90,19 +84,6 @@ export class TypeScriptInternals {
return result?.resolvedModule;
}
/**
* Gets the mode required for module resolution required with the addition of Node16/nodenext
*/
public static getModeForUsageLocation(
file: { impliedNodeFormat?: ts.SourceFile['impliedNodeFormat'] },
usage: ts.StringLiteralLike | undefined,
): ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined {
// Compiler internal:
// https://github.com/microsoft/TypeScript/blob/v4.7.2/src/compiler/program.ts#L568
return (ts as any).getModeForUsageLocation?.(file, usage);
}
/**
* Returns ts.Symbol.parent if it exists.
*/

View File

@@ -1,123 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { FileSystem, PackageJsonLookup, type INodePackageJson, NewlineKind } from '@rushstack/node-core-library';
import { PackageMetadataManager } from '../PackageMetadataManager.js';
const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup();
function resolveInTestPackage(testPackageName: string, ...args: string[]): string {
return path.resolve(__dirname, 'test-data/tsdoc-metadata-path-inference', testPackageName, ...args);
}
function getPackageMetadata(testPackageName: string): {
packageFolder: string;
packageJson: INodePackageJson;
} {
const packageFolder: string = resolveInTestPackage(testPackageName);
const packageJson: INodePackageJson | undefined = packageJsonLookup.tryLoadPackageJsonFor(packageFolder);
if (!packageJson) {
throw new Error('There should be a package.json file in the test package');
}
return { packageFolder, packageJson };
}
function firstArgument(mockFn: jest.Mock): any {
return mockFn.mock.calls[0][0];
}
describe(PackageMetadataManager.name, () => {
describe(PackageMetadataManager.writeTsdocMetadataFile.name, () => {
const originalWriteFile = FileSystem.writeFile;
const mockWriteFile: jest.Mock = jest.fn();
beforeAll(() => {
FileSystem.writeFile = mockWriteFile;
});
afterEach(() => {
mockWriteFile.mockClear();
});
afterAll(() => {
FileSystem.writeFile = originalWriteFile;
});
it('writes the tsdoc metadata file at the provided path', () => {
PackageMetadataManager.writeTsdocMetadataFile('/foo/bar', NewlineKind.CrLf);
expect(firstArgument(mockWriteFile)).toBe('/foo/bar');
});
});
describe(PackageMetadataManager.resolveTsdocMetadataPath.name, () => {
describe('when an empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath = '';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata path as given by "tsdocMetadata" relative to the folder of package.json', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdoc-metadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, packageJson.tsdocMetadata as string),
);
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "typings"', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, path.dirname(packageJson.typings!), 'tsdoc-metadata.json'),
);
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "main"', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, path.dirname(packageJson.main!), 'tsdoc-metadata.json'),
);
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, 'tsdoc-metadata.json'),
);
});
});
});
describe('when a non-empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath = 'path/to/custom-tsdoc-metadata.json';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-tsdocMetadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const { packageFolder, packageJson } = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath)).toBe(
path.resolve(packageFolder, tsdocMetadataPath),
);
});
});
});
});
});

View File

@@ -1,57 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { SyntaxHelpers } from '../SyntaxHelpers.js';
describe(SyntaxHelpers.name, () => {
it(SyntaxHelpers.makeCamelCaseIdentifier.name, () => {
// prettier-ignore
const inputs:string[] = [
'',
'@#(&*^',
'api-extractor-lib1-test',
'one',
'one-two',
'ONE-TWO',
'ONE/two/ /three/FOUR',
'01234'
];
expect(inputs.map((x) => ({ input: x, output: SyntaxHelpers.makeCamelCaseIdentifier(x) }))).toMatchInlineSnapshot(`
Array [
Object {
"input": "",
"output": "_",
},
Object {
"input": "@#(&*^",
"output": "_",
},
Object {
"input": "api-extractor-lib1-test",
"output": "apiExtractorLib1Test",
},
Object {
"input": "one",
"output": "one",
},
Object {
"input": "one-two",
"output": "oneTwo",
},
Object {
"input": "ONE-TWO",
"output": "oneTwo",
},
Object {
"input": "ONE/two/ /three/FOUR",
"output": "oneTwoThreeFour",
},
Object {
"input": "01234",
"output": "_01234",
},
]
`);
});
});

View File

@@ -1,4 +0,0 @@
{
"name": "package-default",
"version": "1.0.0"
}

View File

@@ -1,5 +0,0 @@
{
"name": "package-inferred-from-main",
"version": "1.0.0",
"main": "path/to/main.js"
}

View File

@@ -1,7 +0,0 @@
{
"name": "package-inferred-from-tsdoc-metadata",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts",
"tsdocMetadata": "path/to/tsdoc-metadata.json"
}

View File

@@ -1,6 +0,0 @@
{
"name": "package-inferred-from-typings",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts"
}

View File

@@ -66,9 +66,12 @@ export class CompilerState {
);
}
const inputFilePaths: string[] = commandLine.fileNames.concat(extractorConfig.mainEntryPointFilePath);
const inputFilePaths: string[] = commandLine.fileNames.concat(
extractorConfig.mainEntryPointFilePath.filePath,
extractorConfig.additionalEntryPoints.map((ep) => ep.filePath),
);
if (options?.additionalEntryPoints) {
inputFilePaths.push(...options.additionalEntryPoints);
inputFilePaths.push(...options.additionalEntryPoints.map((entryPoint) => entryPoint));
}
// Append the entry points and remove any non-declaration files from the list

View File

@@ -70,6 +70,11 @@ export const enum ConsoleMessageId {
*/
UsingCustomTSDocConfig = 'console-using-custom-tsdoc-config',
/**
* "Generating ___ API report: ___"
*/
WritingApiReport = 'console-writing-api-report',
/**
* "Writing: ___"
*/

View File

@@ -26,7 +26,7 @@ import { ApiReportGenerator } from '../generators/ApiReportGenerator.js';
import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator.js';
import { CompilerState } from './CompilerState.js';
import { ConsoleMessageId } from './ConsoleMessageId.js';
import { ExtractorConfig } from './ExtractorConfig.js';
import { ExtractorConfig, type IExtractorConfigApiReport } from './ExtractorConfig.js';
import type { ExtractorMessage } from './ExtractorMessage.js';
/**
@@ -263,14 +263,14 @@ export class Extractor {
DocCommentEnhancer.analyze(collector);
ValidationEnhancer.analyze(collector);
const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector);
const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector, extractorConfig);
const apiPackage: ApiPackage = modelBuilder.buildApiPackage();
if (messageRouter.showDiagnostics) {
messageRouter.logDiagnostic(''); // skip a line after any diagnostic messages
}
if (extractorConfig.docModelEnabled) {
if (modelBuilder.docModelEnabled) {
messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, 'Writing: ' + extractorConfig.apiJsonFilePath);
apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, {
toolPackage: Extractor.packageName,
@@ -282,93 +282,22 @@ export class Extractor {
});
}
let apiReportChanged = false;
function writeApiReport(reportConfig: IExtractorConfigApiReport): boolean {
return Extractor._writeApiReport(
collector,
extractorConfig,
messageRouter,
extractorConfig.reportTempFolder,
extractorConfig.reportFolder,
reportConfig,
localBuild,
);
}
let anyReportChanged = false;
if (extractorConfig.apiReportEnabled) {
const actualApiReportPath: string = extractorConfig.reportTempFilePath;
const actualApiReportShortPath: string = extractorConfig._getShortFilePath(extractorConfig.reportTempFilePath);
const expectedApiReportPath: string = extractorConfig.reportFilePath;
const expectedApiReportShortPath: string = extractorConfig._getShortFilePath(extractorConfig.reportFilePath);
const actualApiReportContent: string = ApiReportGenerator.generateReviewFileContent(collector);
// Write the actual file
FileSystem.writeFile(actualApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind,
});
// Compare it against the expected file
if (FileSystem.exists(expectedApiReportPath)) {
const expectedApiReportContent: string = FileSystem.readFile(expectedApiReportPath);
if (ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent)) {
messageRouter.logVerbose(
ConsoleMessageId.ApiReportUnchanged,
`The API report is up to date: ${actualApiReportShortPath}`,
);
} else {
apiReportChanged = true;
if (localBuild) {
// For a local build, just copy the file automatically.
messageRouter.logWarning(
ConsoleMessageId.ApiReportCopied,
`You have changed the public API signature for this project. Updating ${expectedApiReportShortPath}`,
);
FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind,
});
} else {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'You have changed the public API signature for this project.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`,
);
}
}
} else {
// The target file does not exist, so we are setting up the API review file for the first time.
//
// NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder"
// setting, which causes a new file to silently get written to the wrong place. This can be confusing.
// Thus we treat the initial creation of the file specially.
apiReportChanged = true;
if (localBuild) {
const expectedApiReportFolder: string = path.dirname(expectedApiReportPath);
if (FileSystem.exists(expectedApiReportFolder)) {
FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
convertLineEndings: extractorConfig.newlineKind,
});
messageRouter.logWarning(
ConsoleMessageId.ApiReportCreated,
'The API report file was missing, so a new file was created. Please add this file to Git:\n' +
expectedApiReportPath,
);
} else {
messageRouter.logError(
ConsoleMessageId.ApiReportFolderMissing,
'Unable to create the API report file. Please make sure the target folder exists:\n' +
expectedApiReportFolder,
);
}
} else {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'The API report file is missing.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`,
);
}
for (const reportConfig of extractorConfig.reportConfigs) {
anyReportChanged = writeApiReport(reportConfig) || anyReportChanged;
}
}
@@ -421,12 +350,151 @@ export class Extractor {
compilerState,
extractorConfig,
succeeded,
apiReportChanged,
apiReportChanged: anyReportChanged,
errorCount: messageRouter.errorCount,
warningCount: messageRouter.warningCount,
});
}
/**
* Generates the API report at the specified release level, writes it to the specified file path, and compares
* the output to the existing report (if one exists).
*
* @param collector - The collector to get the entities from.
* @param extractorConfig - The configuration for extracting.
* @param messageRouter - The message router to use.
* @param reportTempDirectoryPath - The path to the directory under which the temp report file will be written prior
* to comparison with an existing report.
* @param reportDirectoryPath - The path to the directory under which the existing report file is located, and to
* which the new report will be written post-comparison.
* @param reportConfig - API report configuration, including its file name and {@link ApiReportVariant}.
* @param localBuild - Whether the report is made locally.
* @returns Whether or not the newly generated report differs from the existing report (if one exists).
*/
private static _writeApiReport(
collector: Collector,
extractorConfig: ExtractorConfig,
messageRouter: MessageRouter,
reportTempDirectoryPath: string,
reportDirectoryPath: string,
reportConfig: IExtractorConfigApiReport,
localBuild: boolean,
): boolean {
let apiReportChanged = false;
const actualApiReportPathWithoutExtension: string = path
.resolve(reportTempDirectoryPath, reportConfig.fileName)
.replace(/\.api\.md$/, '');
const expectedApiReportPathWithoutExtension: string = path
.resolve(reportDirectoryPath, reportConfig.fileName)
.replace(/\.api\.md$/, '');
const actualApiReportContentMap: Map<string, string> = ApiReportGenerator.generateReviewFileContent(
collector,
reportConfig.variant,
);
for (const [modulePath, actualApiReportContent] of actualApiReportContentMap) {
const actualEntryPointApiReportPath = `${actualApiReportPathWithoutExtension}${
modulePath ? '.' : ''
}${modulePath}.api.md`;
const actualEntryPointApiReportShortPath: string =
extractorConfig._getShortFilePath(actualEntryPointApiReportPath);
const expectedEntryPointApiReportPath = `${expectedApiReportPathWithoutExtension}${
modulePath ? '.' : ''
}${modulePath}.api.md`;
const expectedEntryPointApiReportShortPath: string = extractorConfig._getShortFilePath(
expectedEntryPointApiReportPath,
);
collector.messageRouter.logVerbose(
ConsoleMessageId.WritingApiReport,
`Generating ${reportConfig.variant} API report: ${expectedEntryPointApiReportPath}`,
);
// Write the actual file
FileSystem.writeFile(actualEntryPointApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind,
});
// Compare it against the expected file
if (FileSystem.exists(expectedEntryPointApiReportPath)) {
const expectedApiReportContent: string = FileSystem.readFile(expectedEntryPointApiReportPath);
if (ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent)) {
messageRouter.logVerbose(
ConsoleMessageId.ApiReportUnchanged,
`The API report is up to date: ${actualEntryPointApiReportShortPath}`,
);
} else {
apiReportChanged = true;
if (localBuild) {
// For a local build, just copy the file automatically.
messageRouter.logWarning(
ConsoleMessageId.ApiReportCopied,
`You have changed the API signature for this project. Updating ${actualEntryPointApiReportShortPath}`,
);
FileSystem.writeFile(actualEntryPointApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind,
});
} else {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'You have changed the API signature for this project.' +
` Please copy the file "${actualEntryPointApiReportShortPath}" to "${expectedEntryPointApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`,
);
}
}
} else {
// The target file does not exist, so we are setting up the API review file for the first time.
//
// NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder"
// setting, which causes a new file to silently get written to the wrong place. This can be confusing.
// Thus we treat the initial creation of the file specially.
apiReportChanged = true;
if (localBuild) {
const expectedApiReportFolder: string = path.dirname(expectedEntryPointApiReportPath);
if (FileSystem.exists(expectedApiReportFolder)) {
FileSystem.writeFile(expectedEntryPointApiReportPath, actualApiReportContent, {
convertLineEndings: extractorConfig.newlineKind,
});
messageRouter.logWarning(
ConsoleMessageId.ApiReportCreated,
'The API report file was missing, so a new file was created. Please add this file to Git:\n' +
expectedEntryPointApiReportPath,
);
} else {
messageRouter.logError(
ConsoleMessageId.ApiReportFolderMissing,
'Unable to create the API report file. Please make sure the target folder exists:\n' +
expectedApiReportFolder,
);
}
} else {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'The API report file is missing.' +
` Please copy the file "${actualEntryPointApiReportShortPath}" to "${expectedEntryPointApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`,
);
}
}
}
return apiReportChanged;
}
private static _checkCompilerCompatibility(extractorConfig: ExtractorConfig, messageRouter: MessageRouter): void {
messageRouter.logInfo(ConsoleMessageId.Preamble, `Analysis will use the bundled TypeScript version ${ts.version}`);

View File

@@ -2,8 +2,8 @@
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { EnumMemberOrder } from '@discordjs/api-extractor-model';
import { TSDocConfiguration } from '@microsoft/tsdoc';
import { EnumMemberOrder, ReleaseTag } from '@discordjs/api-extractor-model';
import { TSDocConfiguration, TSDocTagDefinition } from '@microsoft/tsdoc';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import {
JsonFile,
@@ -17,14 +17,21 @@ import {
Path,
NewlineKind,
} from '@rushstack/node-core-library';
import { type IRigConfig, RigConfig } from '@rushstack/rig-package';
import type { MergeWithCustomizer } from 'lodash';
import cloneDeep from 'lodash/cloneDeep.js';
import merge from 'lodash/merge.js';
import mergeWith from 'lodash/mergeWith.js';
import * as resolve from 'resolve';
import { PackageMetadataManager } from '../analyzer/PackageMetadataManager.js';
import { MessageRouter } from '../collector/MessageRouter.js';
import type { IApiModelGenerationOptions } from '../generators/ApiModelGenerator';
import apiExtractorSchema from '../schemas/api-extractor.schema.json' assert { type: 'json' };
import type { IConfigFile, IExtractorMessagesConfig } from './IConfigFile.js';
import type {
ApiReportVariant,
IConfigApiReport,
IConfigFile,
IConfigEntryPoint,
IExtractorMessagesConfig,
} from './IConfigFile.js';
/**
* Tokens used during variable expansion of path fields from api-extractor.json.
@@ -66,8 +73,10 @@ export interface IExtractorConfigLoadForFolderOptions {
/**
* An already constructed `RigConfig` object. If omitted, then a new `RigConfig` object will be constructed.
*
* @deprecated this is unsupported in the discord.js version
*/
rigConfig?: IRigConfig;
rigConfig?: never;
/**
* The folder path to start from when searching for api-extractor.json.
@@ -147,14 +156,56 @@ export interface IExtractorConfigPrepareOptions {
tsdocConfigFile?: TSDocConfigFile;
}
/**
* Configuration for a single API report, including its {@link IExtractorConfigApiReport.variant}.
*
* @public
*/
export interface IExtractorConfigApiReport {
/**
* Name of the output report file.
*
* @remarks Relative to the configured report directory path.
*/
fileName: string;
/**
* Report variant.
* Determines which API items will be included in the report output, based on their tagged release levels.
*/
variant: ApiReportVariant;
}
/**
* Default {@link IConfigApiReport.reportVariants}
*/
const defaultApiReportVariants: readonly ApiReportVariant[] = ['complete'];
/**
* Default {@link IConfigApiReport.tagsToReport}.
*
* @remarks
* Note that this list is externally documented, and directly affects report output.
* Also note that the order of tags in this list is significant, as it determines the order of tags in the report.
* Any changes to this list should be considered breaking.
*/
const defaultTagsToReport: Readonly<Record<`@${string}`, boolean>> = {
'@sealed': true,
'@virtual': true,
'@override': true,
'@eventProperty': true,
'@deprecated': true,
};
interface IExtractorConfigParameters {
additionalEntryPoints: IConfigEntryPoint[];
alphaTrimmedFilePath: string;
apiJsonFilePath: string;
apiReportEnabled: boolean;
apiReportIncludeForgottenExports: boolean;
betaTrimmedFilePath: string;
bundledPackages: string[];
docModelEnabled: boolean;
docModelGenerationOptions: IApiModelGenerationOptions | undefined;
docModelIncludeForgottenExports: boolean;
enumMemberOrder: EnumMemberOrder;
mainEntryPointFilePath: string;
@@ -167,10 +218,12 @@ interface IExtractorConfigParameters {
projectFolder: string;
projectFolderUrl: string | undefined;
publicTrimmedFilePath: string;
reportFilePath: string;
reportTempFilePath: string;
reportConfigs: readonly IExtractorConfigApiReport[];
reportFolder: string;
reportTempFolder: string;
rollupEnabled: boolean;
skipLibCheck: boolean;
tagsToReport: Readonly<Record<`@${string}`, boolean>>;
testMode: boolean;
tsconfigFilePath: string;
tsdocConfigFile: TSDocConfigFile;
@@ -180,9 +233,22 @@ interface IExtractorConfigParameters {
untrimmedFilePath: string;
}
// Lodash merges array values by default, which is unintuitive for config files (and makes it impossible for derived configurations to overwrite arrays).
// For example, given a base config containing an array property with value ["foo", "bar"] and a derived config that specifies ["baz"] for that property, lodash will produce ["baz", "bar"], which is unintuitive.
// This customizer function ensures that arrays are always overwritten.
const mergeCustomizer: MergeWithCustomizer = (_objValue, srcValue) => {
if (Array.isArray(srcValue)) {
return srcValue;
}
// Fall back to default merge behavior.
return undefined;
};
/**
* The `ExtractorConfig` class loads, validates, interprets, and represents the api-extractor.json config file.
*
* @sealed
* @public
*/
export class ExtractorConfig {
@@ -230,7 +296,12 @@ export class ExtractorConfig {
/**
* {@inheritDoc IConfigFile.mainEntryPointFilePath}
*/
public readonly mainEntryPointFilePath: string;
public readonly mainEntryPointFilePath: IConfigEntryPoint;
/**
* {@inheritDoc IConfigFile.additionalEntryPoints}
*/
public readonly additionalEntryPoints: IConfigEntryPoint[];
/**
* {@inheritDoc IConfigFile.bundledPackages}
@@ -258,14 +329,48 @@ export class ExtractorConfig {
public readonly apiReportEnabled: boolean;
/**
* The `reportFolder` path combined with the `reportFileName`.
* List of configurations for report files to be generated.
*
* @remarks Derived from {@link IConfigApiReport.reportFileName} and {@link IConfigApiReport.reportVariants}.
*/
public readonly reportFilePath: string;
public readonly reportConfigs: readonly IExtractorConfigApiReport[];
/**
* The `reportTempFolder` path combined with the `reportFileName`.
* {@inheritDoc IConfigApiReport.reportFolder}
*/
public readonly reportTempFilePath: string;
public readonly reportFolder: string;
/**
* {@inheritDoc IConfigApiReport.reportTempFolder}
*/
public readonly reportTempFolder: string;
/**
* {@inheritDoc IConfigApiReport.tagsToReport}
*/
public readonly tagsToReport: Readonly<Record<`@${string}`, boolean>>;
/**
* Gets the file path for the "complete" (default) report configuration, if one was specified.
* Otherwise, returns an empty string.
*
* @deprecated Use {@link ExtractorConfig.reportConfigs} to access all report configurations.
*/
public get reportFilePath(): string {
const completeConfig: IExtractorConfigApiReport | undefined = this._getCompleteReportConfig();
return completeConfig === undefined ? '' : path.join(this.reportFolder, completeConfig.fileName);
}
/**
* Gets the temp file path for the "complete" (default) report configuration, if one was specified.
* Otherwise, returns an empty string.
*
* @deprecated Use {@link ExtractorConfig.reportConfigs} to access all report configurations.
*/
public get reportTempFilePath(): string {
const completeConfig: IExtractorConfigApiReport | undefined = this._getCompleteReportConfig();
return completeConfig === undefined ? '' : path.join(this.reportTempFolder, completeConfig.fileName);
}
/**
* {@inheritDoc IConfigApiReport.includeForgottenExports}
@@ -273,9 +378,11 @@ export class ExtractorConfig {
public readonly apiReportIncludeForgottenExports: boolean;
/**
* {@inheritDoc IConfigDocModel.enabled}
* If specified, the doc model is enabled and the specified options will be used.
*
* @beta
*/
public readonly docModelEnabled: boolean;
public readonly docModelGenerationOptions: IApiModelGenerationOptions | undefined;
/**
* {@inheritDoc IConfigDocModel.apiJsonFilePath}
@@ -363,37 +470,77 @@ export class ExtractorConfig {
*/
public readonly enumMemberOrder: EnumMemberOrder;
private constructor(parameters: IExtractorConfigParameters) {
this.projectFolder = parameters.projectFolder;
this.packageJson = parameters.packageJson;
this.packageFolder = parameters.packageFolder;
this.mainEntryPointFilePath = parameters.mainEntryPointFilePath;
this.bundledPackages = parameters.bundledPackages;
this.tsconfigFilePath = parameters.tsconfigFilePath;
this.overrideTsconfig = parameters.overrideTsconfig;
this.skipLibCheck = parameters.skipLibCheck;
this.apiReportEnabled = parameters.apiReportEnabled;
this.reportFilePath = parameters.reportFilePath;
this.reportTempFilePath = parameters.reportTempFilePath;
this.apiReportIncludeForgottenExports = parameters.apiReportIncludeForgottenExports;
this.docModelEnabled = parameters.docModelEnabled;
this.apiJsonFilePath = parameters.apiJsonFilePath;
this.docModelIncludeForgottenExports = parameters.docModelIncludeForgottenExports;
this.projectFolderUrl = parameters.projectFolderUrl;
this.rollupEnabled = parameters.rollupEnabled;
this.untrimmedFilePath = parameters.untrimmedFilePath;
this.alphaTrimmedFilePath = parameters.alphaTrimmedFilePath;
this.betaTrimmedFilePath = parameters.betaTrimmedFilePath;
this.publicTrimmedFilePath = parameters.publicTrimmedFilePath;
this.omitTrimmingComments = parameters.omitTrimmingComments;
this.tsdocMetadataEnabled = parameters.tsdocMetadataEnabled;
this.tsdocMetadataFilePath = parameters.tsdocMetadataFilePath;
this.tsdocConfigFile = parameters.tsdocConfigFile;
this.tsdocConfiguration = parameters.tsdocConfiguration;
this.newlineKind = parameters.newlineKind;
this.messages = parameters.messages;
this.testMode = parameters.testMode;
this.enumMemberOrder = parameters.enumMemberOrder;
private constructor({
projectFolder,
packageJson,
packageFolder,
mainEntryPointFilePath,
additionalEntryPoints,
bundledPackages,
tsconfigFilePath,
overrideTsconfig,
skipLibCheck,
apiReportEnabled,
apiReportIncludeForgottenExports,
reportConfigs,
reportFolder,
reportTempFolder,
tagsToReport,
docModelGenerationOptions,
apiJsonFilePath,
docModelIncludeForgottenExports,
projectFolderUrl,
rollupEnabled,
untrimmedFilePath,
alphaTrimmedFilePath,
betaTrimmedFilePath,
publicTrimmedFilePath,
omitTrimmingComments,
tsdocMetadataEnabled,
tsdocMetadataFilePath,
tsdocConfigFile,
tsdocConfiguration,
newlineKind,
messages,
testMode,
enumMemberOrder,
}: IExtractorConfigParameters) {
this.projectFolder = projectFolder;
this.packageJson = packageJson;
this.packageFolder = packageFolder;
this.mainEntryPointFilePath = {
modulePath: '',
filePath: mainEntryPointFilePath,
};
this.additionalEntryPoints = additionalEntryPoints;
this.bundledPackages = bundledPackages;
this.tsconfigFilePath = tsconfigFilePath;
this.overrideTsconfig = overrideTsconfig;
this.skipLibCheck = skipLibCheck;
this.apiReportEnabled = apiReportEnabled;
this.reportConfigs = reportConfigs;
this.reportFolder = reportFolder;
this.reportTempFolder = reportTempFolder;
this.tagsToReport = tagsToReport;
this.apiReportIncludeForgottenExports = apiReportIncludeForgottenExports;
this.docModelGenerationOptions = docModelGenerationOptions;
this.apiJsonFilePath = apiJsonFilePath;
this.docModelIncludeForgottenExports = docModelIncludeForgottenExports;
this.projectFolderUrl = projectFolderUrl;
this.rollupEnabled = rollupEnabled;
this.untrimmedFilePath = untrimmedFilePath;
this.alphaTrimmedFilePath = alphaTrimmedFilePath;
this.betaTrimmedFilePath = betaTrimmedFilePath;
this.publicTrimmedFilePath = publicTrimmedFilePath;
this.omitTrimmingComments = omitTrimmingComments;
this.tsdocMetadataEnabled = tsdocMetadataEnabled;
this.tsdocMetadataFilePath = tsdocMetadataFilePath;
this.tsdocConfigFile = tsdocConfigFile;
this.tsdocConfiguration = tsdocConfiguration;
this.newlineKind = newlineKind;
this.messages = messages;
this.testMode = testMode;
this.enumMemberOrder = enumMemberOrder;
}
/**
@@ -480,44 +627,8 @@ export class ExtractorConfig {
configFilename = path.join(baseFolder, ExtractorConfig.FILENAME);
if (!FileSystem.exists(configFilename)) {
// If We didn't find it in <packageFolder>/api-extractor.json or <packageFolder>/config/api-extractor.json
// then check for a rig package
if (packageFolder) {
let rigConfig: IRigConfig;
if (options.rigConfig) {
// The caller provided an already solved RigConfig. Double-check that it is for the right project.
if (!Path.isEqual(options.rigConfig.projectFolderPath, packageFolder)) {
throw new Error(
'The provided ILoadForFolderOptions.rigConfig is for the wrong project folder:\n' +
'\nExpected path: ' +
packageFolder +
'\nProvided path: ' +
options.rigConfig.projectFolderOriginalPath,
);
}
rigConfig = options.rigConfig;
} else {
rigConfig = RigConfig.loadForProjectFolder({
projectFolderPath: packageFolder,
});
}
if (rigConfig.rigFound) {
configFilename = path.join(rigConfig.getResolvedProfileFolder(), ExtractorConfig.FILENAME);
// If the "projectFolder" setting isn't specified in api-extractor.json, it defaults to the
// "<lookup>" token which will probe for the tsconfig.json nearest to the api-extractor.json path.
// But this won't work if api-extractor.json belongs to the rig. So instead "<lookup>" should be
// the "<packageFolder>" that referenced the rig.
projectFolderLookupToken = packageFolder;
}
}
if (!FileSystem.exists(configFilename)) {
// API Extractor does not seem to be configured for this folder
return undefined;
}
// API Extractor does not seem to be configured for this folder
return undefined;
}
}
@@ -618,7 +729,7 @@ export class ExtractorConfig {
ExtractorConfig._resolveConfigFileRelativePaths(baseConfig, currentConfigFolderPath);
// Merge extractorConfig into baseConfig, mutating baseConfig
merge(baseConfig, configObject);
mergeWith(baseConfig, configObject, mergeCustomizer);
configObject = baseConfig;
currentConfigFilePath = extendsField;
@@ -628,7 +739,7 @@ export class ExtractorConfig {
}
// Lastly, apply the defaults
configObject = merge(cloneDeep(ExtractorConfig._defaultConfig), configObject);
configObject = mergeWith(cloneDeep(ExtractorConfig._defaultConfig), configObject, mergeCustomizer);
ExtractorConfig.jsonSchema.validateObject(configObject, jsonFilePath);
@@ -653,6 +764,21 @@ export class ExtractorConfig {
);
}
if (configFile.additionalEntryPoints) {
const entryPointWithAbsolutePath: IConfigEntryPoint[] = [];
for (const entryPoint of configFile.additionalEntryPoints) {
const absoluteFilePath: string = ExtractorConfig._resolveConfigFileRelativePath(
'additionalEntryPoints',
entryPoint.filePath,
currentConfigFolderPath,
);
entryPointWithAbsolutePath.push({ ...entryPoint, filePath: absoluteFilePath });
}
configFile.additionalEntryPoints = entryPointWithAbsolutePath;
}
if (configFile.compiler?.tsconfigFilePath) {
configFile.compiler.tsconfigFilePath = ExtractorConfig._resolveConfigFileRelativePath(
'tsconfigFilePath',
@@ -885,6 +1011,25 @@ export class ExtractorConfig {
throw new Error('The "mainEntryPointFilePath" path does not exist: ' + mainEntryPointFilePath);
}
const additionalEntryPoints: IConfigEntryPoint[] = [];
for (const entryPoint of configObject.additionalEntryPoints || []) {
const absoluteEntryPointFilePath: string = ExtractorConfig._resolvePathWithTokens(
'entryPointFilePath',
entryPoint.filePath,
tokenContext,
);
if (!ExtractorConfig.hasDtsFileExtension(absoluteEntryPointFilePath)) {
throw new Error('The "additionalEntryPoints" value is not a declaration file: ' + absoluteEntryPointFilePath);
}
if (!FileSystem.exists(absoluteEntryPointFilePath)) {
throw new Error('The "additionalEntryPoints" path does not exist: ' + absoluteEntryPointFilePath);
}
additionalEntryPoints.push({ ...entryPoint, filePath: absoluteEntryPointFilePath });
}
const bundledPackages: string[] = configObject.bundledPackages ?? [];
for (const bundledPackage of bundledPackages) {
if (!PackageName.isValidName(bundledPackage)) {
@@ -908,51 +1053,92 @@ export class ExtractorConfig {
}
}
let apiReportEnabled = false;
let reportFilePath = '';
let reportTempFilePath = '';
let apiReportIncludeForgottenExports = false;
if (configObject.apiReport) {
apiReportEnabled = Boolean(configObject.apiReport.enabled);
const reportFilename: string = ExtractorConfig._expandStringWithTokens(
'reportFileName',
configObject.apiReport.reportFileName ?? '',
tokenContext,
);
if (!reportFilename) {
// A merged configuration should have this
throw new Error('The "reportFilename" setting is missing');
}
if (reportFilename.includes('/') || reportFilename.includes('\\')) {
// A merged configuration should have this
throw new Error(`The "reportFilename" setting contains invalid characters: "${reportFilename}"`);
}
const reportFolder: string = ExtractorConfig._resolvePathWithTokens(
'reportFolder',
configObject.apiReport.reportFolder,
tokenContext,
);
const reportTempFolder: string = ExtractorConfig._resolvePathWithTokens(
'reportTempFolder',
configObject.apiReport.reportTempFolder,
tokenContext,
);
reportFilePath = path.join(reportFolder, reportFilename);
reportTempFilePath = path.join(reportTempFolder, reportFilename);
apiReportIncludeForgottenExports = Boolean(configObject.apiReport.includeForgottenExports);
if (configObject.apiReport?.tagsToReport) {
_validateTagsToReport(configObject.apiReport.tagsToReport);
}
let docModelEnabled = false;
const apiReportEnabled: boolean = configObject.apiReport?.enabled ?? false;
const apiReportIncludeForgottenExports: boolean = configObject.apiReport?.includeForgottenExports ?? false;
let reportFolder: string = tokenContext.projectFolder;
let reportTempFolder: string = tokenContext.projectFolder;
const reportConfigs: IExtractorConfigApiReport[] = [];
let tagsToReport: Record<`@${string}`, boolean> = {};
if (apiReportEnabled) {
// Undefined case checked above where we assign `apiReportEnabled`
const apiReportConfig: IConfigApiReport = configObject.apiReport!;
const reportFileNameSuffix = '.api.md';
let reportFileNameBase: string;
if (apiReportConfig.reportFileName) {
if (apiReportConfig.reportFileName.includes('/') || apiReportConfig.reportFileName.includes('\\')) {
throw new Error(
`The "reportFileName" setting contains invalid characters: "${apiReportConfig.reportFileName}"`,
);
}
if (apiReportConfig.reportFileName.endsWith(reportFileNameSuffix)) {
// The system previously asked users to specify their filenames in a form containing the `.api.md` extension.
// This guidance has changed, but to maintain backwards compatibility, we will temporarily support input
// that ends with the `.api.md` extension specially, by stripping it out.
// This should be removed in version 8, possibly replaced with an explicit error to help users
// migrate their configs.
reportFileNameBase = apiReportConfig.reportFileName.slice(0, -reportFileNameSuffix.length);
} else {
// `.api.md` extension was not specified. Use provided file name base as is.
reportFileNameBase = apiReportConfig.reportFileName;
}
} else {
// Default value
reportFileNameBase = '<unscopedPackageName>';
}
const reportVariantKinds: readonly ApiReportVariant[] =
apiReportConfig.reportVariants ?? defaultApiReportVariants;
for (const reportVariantKind of reportVariantKinds) {
// Omit the variant kind from the "complete" report file name for simplicity and for backwards compatibility.
const fileNameWithTokens = `${reportFileNameBase}${
reportVariantKind === 'complete' ? '' : `.${reportVariantKind}`
}${reportFileNameSuffix}`;
const normalizedFileName: string = ExtractorConfig._expandStringWithTokens(
'reportFileName',
fileNameWithTokens,
tokenContext,
);
reportConfigs.push({
fileName: normalizedFileName,
variant: reportVariantKind,
});
}
if (apiReportConfig.reportFolder) {
reportFolder = ExtractorConfig._resolvePathWithTokens(
'reportFolder',
apiReportConfig.reportFolder,
tokenContext,
);
}
if (apiReportConfig.reportTempFolder) {
reportTempFolder = ExtractorConfig._resolvePathWithTokens(
'reportTempFolder',
apiReportConfig.reportTempFolder,
tokenContext,
);
}
tagsToReport = {
...defaultTagsToReport,
...apiReportConfig.tagsToReport,
};
}
let docModelGenerationOptions: IApiModelGenerationOptions | undefined;
let apiJsonFilePath = '';
let docModelIncludeForgottenExports = false;
let projectFolderUrl: string | undefined;
if (configObject.docModel) {
docModelEnabled = Boolean(configObject.docModel.enabled);
if (configObject.docModel?.enabled) {
apiJsonFilePath = ExtractorConfig._resolvePathWithTokens(
'apiJsonFilePath',
configObject.docModel.apiJsonFilePath,
@@ -960,6 +1146,43 @@ export class ExtractorConfig {
);
docModelIncludeForgottenExports = Boolean(configObject.docModel.includeForgottenExports);
projectFolderUrl = configObject.docModel.projectFolderUrl;
const releaseTagsToTrim: Set<ReleaseTag> = new Set<ReleaseTag>();
const releaseTagsToTrimOption: string[] = configObject.docModel.releaseTagsToTrim || ['@internal'];
for (const releaseTagToTrim of releaseTagsToTrimOption) {
let releaseTag: ReleaseTag;
switch (releaseTagToTrim) {
case '@internal': {
releaseTag = ReleaseTag.Internal;
break;
}
case '@alpha': {
releaseTag = ReleaseTag.Alpha;
break;
}
case '@beta': {
releaseTag = ReleaseTag.Beta;
break;
}
case '@public': {
releaseTag = ReleaseTag.Public;
break;
}
default: {
throw new Error(`The release tag "${releaseTagToTrim}" is not supported`);
}
}
releaseTagsToTrim.add(releaseTag);
}
docModelGenerationOptions = {
releaseTagsToTrim,
};
}
let tsdocMetadataEnabled = false;
@@ -1014,6 +1237,15 @@ export class ExtractorConfig {
if (configObject.dtsRollup) {
rollupEnabled = Boolean(configObject.dtsRollup.enabled);
// d.ts rollup is not supported when there are more than one entry points.
if (rollupEnabled && additionalEntryPoints.length > 0) {
throw new Error(
`It seems that you have dtsRollup enabled while you also have defined additionalEntryPoints.` +
`dtsRollup is not supported when there are multiple entry points in your package`,
);
}
untrimmedFilePath = ExtractorConfig._resolvePathWithTokens(
'untrimmedFilePath',
configObject.dtsRollup.untrimmedFilePath,
@@ -1057,15 +1289,18 @@ export class ExtractorConfig {
packageJson,
packageFolder,
mainEntryPointFilePath,
additionalEntryPoints,
bundledPackages,
tsconfigFilePath,
overrideTsconfig: configObject.compiler.overrideTsconfig,
skipLibCheck: Boolean(configObject.compiler.skipLibCheck),
apiReportEnabled,
reportFilePath,
reportTempFilePath,
reportConfigs,
reportFolder,
reportTempFolder,
apiReportIncludeForgottenExports,
docModelEnabled,
tagsToReport,
docModelGenerationOptions,
apiJsonFilePath,
docModelIncludeForgottenExports,
projectFolderUrl,
@@ -1121,6 +1356,13 @@ export class ExtractorConfig {
return new ExtractorConfig({ ...extractorConfigParameters, tsdocConfigFile, tsdocConfiguration });
}
/**
* Gets the report configuration for the "complete" (default) report configuration, if one was specified.
*/
private _getCompleteReportConfig(): IExtractorConfigApiReport | undefined {
return this.reportConfigs.find((x) => x.variant === 'complete');
}
private static _resolvePathWithTokens(
fieldName: string,
value: string | undefined,
@@ -1194,3 +1436,47 @@ export class ExtractorConfig {
throw new Error(`The "${fieldName}" value contains extra token characters ("<" or ">"): ${value}`);
}
}
const releaseTags: Set<string> = new Set(['@public', '@alpha', '@beta', '@internal']);
/**
* Validate {@link ExtractorConfig.tagsToReport}.
*/
function _validateTagsToReport(
tagsToReport: Record<string, boolean>,
): asserts tagsToReport is Record<`@${string}`, boolean> {
const includedReleaseTags: string[] = [];
const invalidTags: [string, string][] = []; // tag name, error
for (const tag of Object.keys(tagsToReport)) {
if (releaseTags.has(tag)) {
// If a release tags is specified, regardless of whether it is enabled, we will throw an error.
// Release tags must not be specified.
includedReleaseTags.push(tag);
}
// If the tag is invalid, generate an error string from the inner error message.
try {
TSDocTagDefinition.validateTSDocTagName(tag);
} catch (error) {
invalidTags.push([tag, (error as Error).message]);
}
}
const errorMessages: string[] = [];
for (const includedReleaseTag of includedReleaseTags) {
errorMessages.push(
`${includedReleaseTag}: Release tags are always included in API reports and must not be specified`,
);
}
for (const [invalidTag, innerError] of invalidTags) {
errorMessages.push(`${invalidTag}: ${innerError}`);
}
if (errorMessages.length > 0) {
const errorMessage: string = [`"tagsToReport" contained one or more invalid tags:`, ...errorMessages].join(
'\n\t- ',
);
throw new Error(errorMessage);
}
}

View File

@@ -4,6 +4,21 @@
import type { EnumMemberOrder } from '@discordjs/api-extractor-model';
import type { ExtractorLogLevel } from './ExtractorLogLevel.js';
/**
* Represents an entry point in the package
* Example:
* ```
* {
* modulePath: 'Shape/Square',
* filePath: './dist/Shape/Square/index.d.ts'
* }
* ```
*/
export interface IConfigEntryPoint {
filePath: string;
modulePath: string;
}
/**
* Determines how the TypeScript compiler engine will be invoked by API Extractor.
*
@@ -47,6 +62,13 @@ export interface IConfigCompiler {
tsconfigFilePath?: string;
}
/**
* The allowed variations of API reports.
*
* @public
*/
export type ApiReportVariant = 'alpha' | 'beta' | 'complete' | 'public';
/**
* Configures how the API report files (*.api.md) will be generated.
*
@@ -71,11 +93,19 @@ export interface IConfigApiReport {
includeForgottenExports?: boolean;
/**
* The filename for the API report files. It will be combined with `reportFolder` or `reportTempFolder` to produce
* a full output filename.
* The base filename for the API report files, to be combined with {@link IConfigApiReport.reportFolder} or
* {@link IConfigApiReport.reportTempFolder} to produce the full file path.
*
* @remarks
* The file extension should be ".api.md", and the string should not contain a path separator such as `\` or `/`.
* The `reportFileName` should not include any path separators such as `\` or `/`. The `reportFileName` should
* not include a file extension, since API Extractor will automatically append an appropriate file extension such
* as `.api.md`. If the {@link IConfigApiReport.reportVariants} setting is used, then the file extension includes
* the variant name, for example `my-report.public.api.md` or `my-report.beta.api.md`. The `complete` variant always
* uses the simple extension `my-report.api.md`.
*
* Previous versions of API Extractor required `reportFileName` to include the `.api.md` extension explicitly;
* for backwards compatibility, that is still accepted but will be discarded before applying the above rules.
* @defaultValue `<unscopedPackageName>`
*/
reportFileName?: string;
@@ -104,8 +134,62 @@ export interface IConfigApiReport {
* prepend a folder token such as `<projectFolder>`.
*/
reportTempFolder?: string;
/**
* The set of report variants to generate.
*
* @remarks
* To support different approval requirements for different API levels, multiple "variants" of the API report can
* be generated. The `reportVariants` setting specifies a list of variants to be generated. If omitted,
* by default only the `complete` variant will be generated, which includes all `@internal`, `@alpha`, `@beta`,
* and `@public` items. Other possible variants are `alpha` (`@alpha` + `@beta` + `@public`),
* `beta` (`@beta` + `@public`), and `public` (`@public only`).
*
* The resulting API report file names will be derived from the {@link IConfigApiReport.reportFileName}.
* @defaultValue `[ "complete" ]`
*/
reportVariants?: ApiReportVariant[];
/**
* Specifies a list of {@link https://tsdoc.org/ | TSDoc} tags that should be reported in the API report file for
* items whose documentation contains them.
*
* @remarks
* Tag names must begin with `@`.
*
* This list may include standard TSDoc tags as well as custom ones.
* For more information on defining custom TSDoc tags, see
* {@link https://api-extractor.com/pages/configs/tsdoc_json/#defining-your-own-tsdoc-tags | here}.
*
* Note that an item's release tag will always reported; this behavior cannot be overridden.
* @defaultValue `@sealed`, `@virtual`, `@override`, `@eventProperty`, and `@deprecated`
* @example Omitting default tags
* To omit the `@sealed` and `@virtual` tags from API reports, you would specify `tagsToReport` as follows:
* ```json
* "tagsToReport": {
* "@sealed": false,
* "@virtual": false
* }
* ```
* @example Including additional tags
* To include additional tags to the set included in API reports, you could specify `tagsToReport` like this:
* ```json
* "tagsToReport": {
* "@customTag": true
* }
* ```
* This will result in `@customTag` being included in addition to the default tags.
*/
tagsToReport?: Readonly<Record<`@${string}`, boolean>>;
}
/**
* The allowed release tags that can be used to mark API items.
*
* @public
*/
export type ReleaseTagForTrim = '@alpha' | '@beta' | '@internal' | '@public';
/**
* Configures how the doc model file (*.api.json) will be generated.
*
@@ -151,6 +235,13 @@ export interface IConfigDocModel {
* Can be omitted if you don't need source code links in your API documentation reference.
*/
projectFolderUrl?: string;
/**
* Specifies a list of release tags that will be trimmed from the doc model.
*
* @defaultValue `["@internal"]`
*/
releaseTagsToTrim?: ReleaseTagForTrim[];
}
/**
@@ -322,6 +413,11 @@ export interface IExtractorMessagesConfig {
* @public
*/
export interface IConfigFile {
/**
* support multiple entry points.
*/
additionalEntryPoints: IConfigEntryPoint[];
/**
* {@inheritDoc IConfigApiReport}
*/

View File

@@ -1,55 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { StandardTags } from '@microsoft/tsdoc';
import { ExtractorConfig } from '../ExtractorConfig.js';
const testDataFolder: string = path.join(__dirname, 'test-data');
describe('Extractor-custom-tags', () => {
describe('should use a TSDocConfiguration', () => {
it.only("with custom TSDoc tags defined in the package's tsdoc.json", () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json'),
);
const { tsdocConfiguration } = extractorConfig;
expect(tsdocConfiguration.tryGetTagDefinition('@block')).not.toBe(undefined);
expect(tsdocConfiguration.tryGetTagDefinition('@inline')).not.toBe(undefined);
expect(tsdocConfiguration.tryGetTagDefinition('@modifier')).not.toBe(undefined);
});
it.only("with custom TSDoc tags enabled per the package's tsdoc.json", () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json'),
);
const { tsdocConfiguration } = extractorConfig;
const block = tsdocConfiguration.tryGetTagDefinition('@block')!;
const inline = tsdocConfiguration.tryGetTagDefinition('@inline')!;
const modifier = tsdocConfiguration.tryGetTagDefinition('@modifier')!;
expect(tsdocConfiguration.isTagSupported(block)).toBe(true);
expect(tsdocConfiguration.isTagSupported(inline)).toBe(true);
expect(tsdocConfiguration.isTagSupported(modifier)).toBe(false);
});
it.only("with standard tags and API Extractor custom tags defined and supported when the package's tsdoc.json extends API Extractor's tsdoc.json", () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json'),
);
const { tsdocConfiguration } = extractorConfig;
expect(tsdocConfiguration.tryGetTagDefinition('@inline')).not.toBe(undefined);
expect(tsdocConfiguration.tryGetTagDefinition('@block')).not.toBe(undefined);
expect(tsdocConfiguration.tryGetTagDefinition('@modifier')).not.toBe(undefined);
for (const tag of StandardTags.allDefinitions.concat([
tsdocConfiguration.tryGetTagDefinition('@betaDocumentation')!,
tsdocConfiguration.tryGetTagDefinition('@internalRemarks')!,
tsdocConfiguration.tryGetTagDefinition('@preapproved')!,
])) {
expect(tsdocConfiguration.tagDefinitions.includes(tag));
expect(tsdocConfiguration.supportedTagDefinitions.includes(tag));
}
});
});
});

View File

@@ -1,36 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { Path } from '@rushstack/node-core-library';
import { ExtractorConfig } from '../ExtractorConfig.js';
const testDataFolder: string = path.join(__dirname, 'test-data');
function expectEqualPaths(path1: string, path2: string): void {
if (!Path.isEqual(path1, path2)) {
fail('Expected paths to be equal:\npath1: ' + path1 + '\npath2: ' + path2);
}
}
// Tests for expanding the "<lookup>" token for the "projectFolder" setting in api-extractor.json
describe(`${ExtractorConfig.name}.${ExtractorConfig.loadFileAndPrepare.name}`, () => {
it.only('config-lookup1: looks up ./api-extractor.json', () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'config-lookup1/api-extractor.json'),
);
expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup1'));
});
it.only('config-lookup2: looks up ./config/api-extractor.json', () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'config-lookup2/config/api-extractor.json'),
);
expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup2'));
});
it.only('config-lookup3a: looks up ./src/test/config/api-extractor.json', () => {
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
path.join(testDataFolder, 'config-lookup3/src/test/config/api-extractor.json'),
);
expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup3/src/test/'));
});
});

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/index.d.ts",
"apiReport": {
"enabled": true
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
}
}

View File

@@ -1,2 +0,0 @@
/* eslint-disable unicorn/no-empty-file */
// empty file

View File

@@ -1,4 +0,0 @@
{
"name": "config-lookup1",
"version": "1.0.0"
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "lib"]
}

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/index.d.ts",
"apiReport": {
"enabled": true
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
}
}

View File

@@ -1,2 +0,0 @@
/* eslint-disable unicorn/no-empty-file */
// empty file

View File

@@ -1,4 +0,0 @@
{
"name": "config-lookup2",
"version": "1.0.0"
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "lib"]
}

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/index.d.ts",
"apiReport": {
"enabled": true
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
}
}

View File

@@ -1,2 +0,0 @@
/* eslint-disable unicorn/no-empty-file */
// empty file

View File

@@ -1,4 +0,0 @@
{
"name": "config-lookup3",
"version": "1.0.0"
}

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/index.d.ts",
"apiReport": {
"enabled": true
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
}
}

View File

@@ -1,2 +0,0 @@
/* eslint-disable unicorn/no-empty-file */
// empty file

View File

@@ -1,25 +0,0 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "lib"]
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "lib"]
}

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/index.d.ts",
"apiReport": {
"enabled": true
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
}
}

View File

@@ -1,7 +0,0 @@
/* eslint-disable tsdoc/syntax */
/**
* @block
* @inline test
* @modifier
*/
interface CustomTagsTestInterface {}

View File

@@ -1,4 +0,0 @@
{
"name": "config-lookup1",
"version": "1.0.0"
}

View File

@@ -1,25 +0,0 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"jsx": "react",
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "lib"]
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"extends": ["../../../../../extends/tsdoc-base.json"],
"tagDefinitions": [
{
"tagName": "@block",
"syntaxKind": "block"
},
{
"tagName": "@inline",
"syntaxKind": "inline",
"allowMultiple": true
},
{
"tagName": "@modifier",
"syntaxKind": "modifier"
}
],
"supportForTags": {
"@block": true,
"@inline": true,
"@modifier": false
}
}

View File

@@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.
import * as os from 'node:os';
import process from 'node:process';
import { InternalError } from '@rushstack/node-core-library';
import { CommandLineParser, type CommandLineFlagParameter } from '@rushstack/ts-command-line';
import colors from 'colors';
@@ -30,23 +31,22 @@ export class ApiExtractorCommandLine extends CommandLineParser {
});
}
protected override async onExecute(): Promise<void> {
protected override async onExecuteAsync(): Promise<void> {
// override
if (this._debugParameter.value) {
InternalError.breakInDebugger = true;
}
process.exitCode = 1;
try {
await super.onExecute();
await super.onExecuteAsync();
process.exitCode = 0;
} catch (error: any) {
if (this._debugParameter.value) {
console.error(os.EOL + error.stack);
} else {
console.error(os.EOL + colors.red('ERROR: ' + error.message.trim()));
}
// eslint-disable-next-line no-restricted-globals
process.exitCode = 1;
}
}

View File

@@ -20,8 +20,7 @@ export class InitAction extends CommandLineAction {
});
}
protected async onExecute(): Promise<void> {
// override
protected override async onExecuteAsync(): Promise<void> {
const inputFilePath: string = path.resolve(__dirname, './schemas/api-extractor-template.json');
const outputFilePath: string = path.resolve(ExtractorConfig.FILENAME);

View File

@@ -83,8 +83,7 @@ export class RunAction extends CommandLineAction {
});
}
protected async onExecute(): Promise<void> {
// override
protected override async onExecuteAsync(): Promise<void> {
const lookup: PackageJsonLookup = new PackageJsonLookup();
let configFilename: string;

View File

@@ -9,7 +9,7 @@ import { PackageDocComment } from '../aedoc/PackageDocComment.js';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModule, AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { AstModule, IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstReferenceResolver } from '../analyzer/AstReferenceResolver.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
@@ -18,12 +18,14 @@ import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js';
import { TypeScriptInternals, type IGlobalVariableAnalyzer } from '../analyzer/TypeScriptInternals.js';
import { ExtractorConfig } from '../api/ExtractorConfig.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { IConfigEntryPoint } from '../api/IConfigFile';
import { ApiItemMetadata, type IApiItemMetadataOptions } from './ApiItemMetadata.js';
import { CollectorEntity } from './CollectorEntity.js';
import { type DeclarationMetadata, InternalDeclarationMetadata } from './DeclarationMetadata.js';
import type { MessageRouter } from './MessageRouter.js';
import type { SourceMapper } from './SourceMapper.js';
import { SymbolMetadata } from './SymbolMetadata.js';
import type { IWorkingPackageEntryPoint } from './WorkingPackage.js';
import { WorkingPackage } from './WorkingPackage.js';
/**
@@ -83,12 +85,15 @@ export class Collector {
private readonly _tsdocParser: tsdoc.TSDocParser;
private _astEntryPoint: AstModule | undefined;
private readonly _entities: CollectorEntity[] = [];
private _astEntryPoints: AstModule[] | undefined;
private readonly _entitiesByAstEntity: Map<AstEntity, CollectorEntity> = new Map<AstEntity, CollectorEntity>();
private readonly _entitiesByAstEntryPoint: Map<IWorkingPackageEntryPoint, CollectorEntity[]> = new Map<
IWorkingPackageEntryPoint,
CollectorEntity[]
>();
private readonly _entitiesBySymbol: Map<ts.Symbol, CollectorEntity> = new Map<ts.Symbol, CollectorEntity>();
private readonly _starExportedExternalModulePaths: string[] = [];
@@ -107,13 +112,23 @@ export class Collector {
this.extractorConfig = options.extractorConfig;
this.sourceMapper = options.sourceMapper;
const entryPointSourceFile: ts.SourceFile | undefined = options.program.getSourceFile(
const entryPoints: IConfigEntryPoint[] = [
this.extractorConfig.mainEntryPointFilePath,
);
...this.extractorConfig.additionalEntryPoints,
];
if (!entryPointSourceFile) {
throw new Error('Unable to load file: ' + this.extractorConfig.mainEntryPointFilePath);
}
const workingPackageEntryPoints: IWorkingPackageEntryPoint[] = entryPoints.map((entryPoint) => {
const sourceFile: ts.SourceFile | undefined = options.program.getSourceFile(entryPoint.filePath);
if (!sourceFile) {
throw new Error('Unable to load file: ' + entryPoint.filePath);
}
return {
modulePath: entryPoint.modulePath,
sourceFile,
};
});
if (!this.extractorConfig.packageFolder || !this.extractorConfig.packageJson) {
throw new Error('Unable to find a package.json file for the project being analyzed');
@@ -122,7 +137,7 @@ export class Collector {
this.workingPackage = new WorkingPackage({
packageFolder: this.extractorConfig.packageFolder,
packageJson: this.extractorConfig.packageJson,
entryPointSourceFile,
entryPoints: workingPackageEntryPoints,
});
this.messageRouter = options.messageRouter;
@@ -169,8 +184,8 @@ export class Collector {
return this._dtsLibReferenceDirectives;
}
public get entities(): readonly CollectorEntity[] {
return this._entities;
public get entities(): ReadonlyMap<IWorkingPackageEntryPoint, CollectorEntity[]> {
return this._entitiesByAstEntryPoint;
}
/**
@@ -185,7 +200,7 @@ export class Collector {
* Perform the analysis.
*/
public analyze(): void {
if (this._astEntryPoint) {
if (this._astEntryPoints) {
throw new Error('DtsRollupGenerator.analyze() was already called');
}
@@ -230,59 +245,101 @@ export class Collector {
);
}
// Build the entry point
const entryPointSourceFile: ts.SourceFile = this.workingPackage.entryPointSourceFile;
// Build entry points
for (const entryPoint of this.workingPackage.entryPoints) {
const { sourceFile: entryPointSourceFile } = entryPoint;
const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile);
this._astEntryPoint = astEntryPoint;
const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile);
const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile(
entryPointSourceFile,
this,
);
if (this._astEntryPoints) {
this._astEntryPoints.push(astEntryPoint);
} else {
this._astEntryPoints = [astEntryPoint];
}
if (packageDocCommentTextRange) {
const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
entryPointSourceFile.text,
packageDocCommentTextRange.pos,
packageDocCommentTextRange.end,
);
if (!this._entitiesByAstEntryPoint.has(entryPoint)) {
this._entitiesByAstEntryPoint.set(entryPoint, []);
}
this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range);
// Process pacakgeDocComment only for the default entry point
if (this.workingPackage.isDefaultEntryPoint(entryPoint)) {
const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile(
entryPointSourceFile,
this,
);
this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile);
if (packageDocCommentTextRange) {
const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
entryPointSourceFile.text,
packageDocCommentTextRange.pos,
packageDocCommentTextRange.end,
);
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}
this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range);
const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);
this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile);
// Create a CollectorEntity for each top-level export.
const processedAstEntities: AstEntity[] = [];
for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) {
this._createCollectorEntity(astEntity, exportName);
processedAstEntities.push(astEntity);
}
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}
}
// Recursively create the remaining CollectorEntities after the top-level entities
// have been processed.
const alreadySeenAstEntities: Set<AstEntity> = new Set<AstEntity>();
for (const astEntity of processedAstEntities) {
this._recursivelyCreateEntities(astEntity, alreadySeenAstEntities);
if (astEntity instanceof AstSymbol) {
this.fetchSymbolMetadata(astEntity);
const { exportedLocalEntities, starExportedExternalModules, visitedAstModules }: IAstModuleExportInfo =
this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);
// Create a CollectorEntity for each top-level export.
const processedAstEntities: AstEntity[] = [];
for (const [exportName, astEntity] of exportedLocalEntities) {
this._createCollectorEntity(astEntity, entryPoint, exportName);
processedAstEntities.push(astEntity);
}
// Recursively create the remaining CollectorEntities after the top-level entities
// have been processed.
const alreadySeenAstEntities: Set<AstEntity> = new Set<AstEntity>();
for (const astEntity of processedAstEntities) {
this._recursivelyCreateEntities(astEntity, entryPoint, alreadySeenAstEntities);
if (astEntity instanceof AstSymbol) {
this.fetchSymbolMetadata(astEntity);
}
}
// Ensure references are collected from any intermediate files that
// only include exports
const nonExternalSourceFiles: Set<ts.SourceFile> = new Set();
for (const { sourceFile, isExternal } of visitedAstModules) {
if (!nonExternalSourceFiles.has(sourceFile) && !isExternal) {
nonExternalSourceFiles.add(sourceFile);
}
}
// Here, we're collecting reference directives from all non-external source files
// that were encountered while looking for exports, but only those references that
// were explicitly written by the developer and marked with the `preserve="true"`
// attribute. In TS >= 5.5, only references that are explicitly authored and marked
// with `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The `_collectReferenceDirectives` function pulls in all references in files that
// contain definitions, but does not examine files that only reexport from other
// files. Here, we're looking through files that were missed by `_collectReferenceDirectives`,
// but only collecting references that were explicitly marked with `preserve="true"`.
// It is intuitive for developers to include references that they explicitly want part of
// their public API in a file like the entrypoint, which is likely to only contain reexports,
// and this picks those up.
this._collectReferenceDirectivesFromSourceFiles(nonExternalSourceFiles, true);
this._makeUniqueNames(entryPoint);
for (const starExportedExternalModule of starExportedExternalModules) {
if (starExportedExternalModule.externalModulePath !== undefined) {
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
}
}
}
this._makeUniqueNames();
for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) {
if (starExportedExternalModule.externalModulePath !== undefined) {
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
}
for (const entities of this._entitiesByAstEntryPoint.values()) {
Sort.sortBy(entities, (x) => x.getSortKey());
}
Sort.sortBy(this._entities, (x) => x.getSortKey());
Sort.sortSet(this._dtsTypeReferenceDirectives);
Sort.sortSet(this._dtsLibReferenceDirectives);
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
@@ -433,7 +490,12 @@ export class Collector {
return overloadIndex;
}
private _createCollectorEntity(astEntity: AstEntity, exportName?: string, parent?: CollectorEntity): CollectorEntity {
private _createCollectorEntity(
astEntity: AstEntity,
entryPoint: IWorkingPackageEntryPoint,
exportName?: string | undefined,
parent?: CollectorEntity,
): void {
let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
if (!entity) {
@@ -446,10 +508,16 @@ export class Collector {
this._entitiesBySymbol.set(astEntity.symbol, entity);
}
this._entities.push(entity);
this._collectReferenceDirectives(astEntity);
}
// add collectorEntity to the corresponding entry point
const entitiesOfAstModule: CollectorEntity[] = this._entitiesByAstEntryPoint.get(entryPoint) || [];
if (!entitiesOfAstModule.includes(entity)) {
entitiesOfAstModule.push(entity);
this._entitiesByAstEntryPoint.set(entryPoint, entitiesOfAstModule);
}
if (exportName) {
if (parent) {
entity.addLocalExportName(exportName, parent);
@@ -457,11 +525,13 @@ export class Collector {
entity.addExportName(exportName);
}
}
return entity;
}
private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set<AstEntity>): void {
private _recursivelyCreateEntities(
astEntity: AstEntity,
entryPoint: IWorkingPackageEntryPoint,
alreadySeenAstEntities: Set<AstEntity>,
): void {
if (alreadySeenAstEntities.has(astEntity)) return;
alreadySeenAstEntities.add(astEntity);
@@ -473,19 +543,19 @@ export class Collector {
// nested inside a namespace, only the namespace gets a collector entity. Note that this
// is not true for AstNamespaceImports below.
if (referencedAstEntity.parentAstSymbol === undefined) {
this._createCollectorEntity(referencedAstEntity);
this._createCollectorEntity(referencedAstEntity, entryPoint);
}
} else {
this._createCollectorEntity(referencedAstEntity);
this._createCollectorEntity(referencedAstEntity, entryPoint);
}
this._recursivelyCreateEntities(referencedAstEntity, alreadySeenAstEntities);
this._recursivelyCreateEntities(referencedAstEntity, entryPoint, alreadySeenAstEntities);
}
});
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
const parentEntity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
if (!parentEntity) {
// This should never happen, as we've already created entities for all AstNamespaceImports.
@@ -496,16 +566,16 @@ export class Collector {
for (const [localExportName, localAstEntity] of astModuleExportInfo.exportedLocalEntities) {
// Create a CollectorEntity for each local export within an AstNamespaceImport entity.
this._createCollectorEntity(localAstEntity, localExportName, parentEntity);
this._recursivelyCreateEntities(localAstEntity, alreadySeenAstEntities);
this._createCollectorEntity(localAstEntity, entryPoint, localExportName, parentEntity);
this._recursivelyCreateEntities(localAstEntity, entryPoint, alreadySeenAstEntities);
}
}
}
/**
* Ensures a unique name for each item in the package typings file.
* Ensures a unique name for each item in the entry point typings file.
*/
private _makeUniqueNames(): void {
private _makeUniqueNames(entryPoint: IWorkingPackageEntryPoint): void {
// The following examples illustrate the nameForEmit heuristics:
//
// Example 1:
@@ -527,8 +597,10 @@ export class Collector {
// Set of names that should NOT be used when generating a unique nameForEmit
const usedNames: Set<string> = new Set<string>();
const entities: CollectorEntity[] = this._entitiesByAstEntryPoint.get(entryPoint) || [];
// First collect the names of explicit package exports, and perform a sanity check.
for (const entity of this._entities) {
for (const entity of entities) {
for (const exportName of entity.exportNames) {
if (usedNames.has(exportName)) {
// This should be impossible
@@ -540,7 +612,7 @@ export class Collector {
}
// Ensure that each entity has a unique nameForEmit
for (const entity of this._entities) {
for (const entity of entities) {
// What name would we ideally want to emit it as?
let idealNameForEmit: string;
@@ -917,37 +989,80 @@ export class Collector {
}
private _collectReferenceDirectives(astEntity: AstEntity): void {
// Here, we're collecting reference directives from source files that contain extracted
// definitions (i.e. - files that contain `export class ...`, `export interface ...`, ...).
// These references may or may not include the `preserve="true" attribute. In TS < 5.5,
// references that end up in .D.TS files may or may not be explicity written by the developer.
// In TS >= 5.5, only references that are explicitly authored and are marked with
// `preserve="true"` are included in the output. See https://github.com/microsoft/TypeScript/pull/57681
//
// The calls to `_collectReferenceDirectivesFromSourceFiles` in this function are
// preserving existing behavior, which is to include all reference directives
// regardless of whether they are explicitly authored or not, but only in files that
// contain definitions.
if (astEntity instanceof AstSymbol) {
const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) =>
astDeclaration.declaration.getSourceFile(),
);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
return;
}
if (astEntity instanceof AstNamespaceImport) {
const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile];
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
this._collectReferenceDirectivesFromSourceFiles(sourceFiles, false);
}
}
private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void {
private _collectReferenceDirectivesFromSourceFiles(
sourceFiles: Iterable<ts.SourceFile>,
onlyIncludeExplicitlyPreserved: boolean,
): void {
const seenFilenames: Set<string> = new Set<string>();
for (const sourceFile of sourceFiles) {
if (sourceFile?.fileName && !seenFilenames.has(sourceFile.fileName)) {
seenFilenames.add(sourceFile.fileName);
if (sourceFile?.fileName) {
const { fileName, typeReferenceDirectives, libReferenceDirectives, text: sourceFileText } = sourceFile;
if (!seenFilenames.has(fileName)) {
seenFilenames.add(fileName);
for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) {
const name: string = sourceFile.text.slice(typeReferenceDirective.pos, typeReferenceDirective.end);
this._dtsTypeReferenceDirectives.add(name);
}
for (const typeReferenceDirective of typeReferenceDirectives) {
const name: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
typeReferenceDirective,
onlyIncludeExplicitlyPreserved,
);
if (name) {
this._dtsTypeReferenceDirectives.add(name);
}
}
for (const libReferenceDirective of sourceFile.libReferenceDirectives) {
const name: string = sourceFile.text.slice(libReferenceDirective.pos, libReferenceDirective.end);
this._dtsLibReferenceDirectives.add(name);
for (const libReferenceDirective of libReferenceDirectives) {
const reference: string | undefined = this._getReferenceDirectiveFromSourceFile(
sourceFileText,
libReferenceDirective,
onlyIncludeExplicitlyPreserved,
);
if (reference) {
this._dtsLibReferenceDirectives.add(reference);
}
}
}
}
}
}
private _getReferenceDirectiveFromSourceFile(
sourceFileText: string,
{ pos, end, preserve }: ts.FileReference,
onlyIncludeExplicitlyPreserved: boolean,
): string | undefined {
const reference: string = sourceFileText.slice(pos, end);
if (preserve || !onlyIncludeExplicitlyPreserved) {
return reference;
}
return undefined;
}
}

View File

@@ -9,11 +9,16 @@ import type * as ts from 'typescript';
* Constructor options for WorkingPackage
*/
export interface IWorkingPackageOptions {
entryPointSourceFile: ts.SourceFile;
entryPoints: IWorkingPackageEntryPoint[];
packageFolder: string;
packageJson: INodePackageJson;
}
export interface IWorkingPackageEntryPoint {
modulePath: string;
sourceFile: ts.SourceFile;
}
/**
* Information about the working package for a particular invocation of API Extractor.
*
@@ -51,7 +56,7 @@ export class WorkingPackage {
* only processes a single entry point during an invocation. This will be improved
* in the future.
*/
public readonly entryPointSourceFile: ts.SourceFile;
public readonly entryPoints: IWorkingPackageEntryPoint[];
/**
* The `@packageDocumentation` comment, if any, for the working package.
@@ -66,7 +71,7 @@ export class WorkingPackage {
public constructor(options: IWorkingPackageOptions) {
this.packageFolder = options.packageFolder;
this.packageJson = options.packageJson;
this.entryPointSourceFile = options.entryPointSourceFile;
this.entryPoints = options.entryPoints;
}
/**
@@ -75,4 +80,8 @@ export class WorkingPackage {
public get name(): string {
return this.packageJson.name;
}
public isDefaultEntryPoint(entryPoint: IWorkingPackageEntryPoint): boolean {
return entryPoint.modulePath === '';
}
}

View File

@@ -11,6 +11,7 @@ import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import { VisitorState } from '../collector/VisitorState.js';
import type { IWorkingPackageEntryPoint } from '../collector/WorkingPackage.js';
export class DocCommentEnhancer {
private readonly _collector: Collector;
@@ -25,21 +26,23 @@ export class DocCommentEnhancer {
}
public analyze(): void {
for (const entity of this._collector.entities) {
if (
entity.astEntity instanceof AstSymbol &&
(entity.consumable ||
this._collector.extractorConfig.apiReportIncludeForgottenExports ||
this._collector.extractorConfig.docModelIncludeForgottenExports)
) {
entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
this._analyzeApiItem(astDeclaration);
});
for (const [entryPoint, entities] of this._collector.entities) {
for (const entity of entities) {
if (
entity.astEntity instanceof AstSymbol &&
(entity.consumable ||
this._collector.extractorConfig.apiReportIncludeForgottenExports ||
this._collector.extractorConfig.docModelIncludeForgottenExports)
) {
entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
this._analyzeApiItem(astDeclaration, entryPoint);
});
}
}
}
}
private _analyzeApiItem(astDeclaration: AstDeclaration): void {
private _analyzeApiItem(astDeclaration: AstDeclaration, entryPoint: IWorkingPackageEntryPoint): void {
const metadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
if (metadata.docCommentEnhancerVisitorState === VisitorState.Visited) {
return;
@@ -57,12 +60,12 @@ export class DocCommentEnhancer {
metadata.docCommentEnhancerVisitorState = VisitorState.Visiting;
if (metadata.tsdocComment?.inheritDocTag) {
this._applyInheritDoc(astDeclaration, metadata.tsdocComment, metadata.tsdocComment.inheritDocTag);
this._applyInheritDoc(astDeclaration, metadata.tsdocComment, metadata.tsdocComment.inheritDocTag, entryPoint);
}
this._analyzeNeedsDocumentation(astDeclaration, metadata);
this._checkForBrokenLinks(astDeclaration, metadata);
this._checkForBrokenLinks(astDeclaration, metadata, entryPoint);
metadata.docCommentEnhancerVisitorState = VisitorState.Visited;
}
@@ -141,15 +144,23 @@ export class DocCommentEnhancer {
}
}
private _checkForBrokenLinks(astDeclaration: AstDeclaration, metadata: ApiItemMetadata): void {
private _checkForBrokenLinks(
astDeclaration: AstDeclaration,
metadata: ApiItemMetadata,
entryPoint: IWorkingPackageEntryPoint,
): void {
if (!metadata.tsdocComment) {
return;
}
this._checkForBrokenLinksRecursive(astDeclaration, metadata.tsdocComment);
this._checkForBrokenLinksRecursive(astDeclaration, metadata.tsdocComment, entryPoint);
}
private _checkForBrokenLinksRecursive(astDeclaration: AstDeclaration, node: tsdoc.DocNode): void {
private _checkForBrokenLinksRecursive(
astDeclaration: AstDeclaration,
node: tsdoc.DocNode,
entryPoint: IWorkingPackageEntryPoint,
): void {
if (
node instanceof tsdoc.DocLinkTag &&
node.codeDestination && // Is it referring to the working package? If not, we don't do any link validation, because
@@ -160,6 +171,7 @@ export class DocCommentEnhancer {
) {
const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve(
node.codeDestination,
entryPoint,
);
if (referencedAstDeclaration instanceof ResolverFailure) {
@@ -172,7 +184,7 @@ export class DocCommentEnhancer {
}
for (const childNode of node.getChildNodes()) {
this._checkForBrokenLinksRecursive(astDeclaration, childNode);
this._checkForBrokenLinksRecursive(astDeclaration, childNode, entryPoint);
}
}
@@ -183,6 +195,7 @@ export class DocCommentEnhancer {
astDeclaration: AstDeclaration,
docComment: tsdoc.DocComment,
inheritDocTag: tsdoc.DocInheritDocTag,
entryPoint: IWorkingPackageEntryPoint,
): void {
if (!inheritDocTag.declarationReference) {
this._collector.messageRouter.addAnalyzerIssue(
@@ -208,6 +221,7 @@ export class DocCommentEnhancer {
const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve(
inheritDocTag.declarationReference,
entryPoint,
);
if (referencedAstDeclaration instanceof ResolverFailure) {
@@ -219,7 +233,7 @@ export class DocCommentEnhancer {
return;
}
this._analyzeApiItem(referencedAstDeclaration);
this._analyzeApiItem(referencedAstDeclaration, entryPoint);
const referencedMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(referencedAstDeclaration);

View File

@@ -6,7 +6,7 @@ import { ReleaseTag, releaseTagCompare, releaseTagGetTagName } from '@discordjs/
import * as ts from 'typescript';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
@@ -14,53 +14,56 @@ import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import type { CollectorEntity } from '../collector/CollectorEntity.js';
import type { SymbolMetadata } from '../collector/SymbolMetadata.js';
import type { IWorkingPackageEntryPoint } from '../collector/WorkingPackage.js';
export class ValidationEnhancer {
public static analyze(collector: Collector): void {
const alreadyWarnedEntities: Set<AstEntity> = new Set<AstEntity>();
for (const entity of collector.entities) {
if (
!(
entity.consumable ||
collector.extractorConfig.apiReportIncludeForgottenExports ||
collector.extractorConfig.docModelIncludeForgottenExports
)
) {
continue;
}
for (const [entryPoint, entities] of collector.entities) {
for (const entity of entities) {
if (
!(
entity.consumable ||
collector.extractorConfig.apiReportIncludeForgottenExports ||
collector.extractorConfig.docModelIncludeForgottenExports
)
) {
continue;
}
if (entity.astEntity instanceof AstSymbol) {
// A regular exported AstSymbol
if (entity.astEntity instanceof AstSymbol) {
// A regular exported AstSymbol
const astSymbol: AstSymbol = entity.astEntity;
const astSymbol: AstSymbol = entity.astEntity;
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities, entryPoint);
});
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
} else if (entity.astEntity instanceof AstNamespaceImport) {
// A namespace created using "import * as ___ from ___"
const astNamespaceImport: AstNamespaceImport = entity.astEntity;
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
} else if (entity.astEntity instanceof AstNamespaceImport) {
// A namespace created using "import * as ___ from ___"
const astNamespaceImport: AstNamespaceImport = entity.astEntity;
const astModuleExportInfo: AstModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector);
const astModuleExportInfo: IAstModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector);
for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
if (namespaceMemberAstEntity instanceof AstSymbol) {
const astSymbol: AstSymbol = namespaceMemberAstEntity;
for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
if (namespaceMemberAstEntity instanceof AstSymbol) {
const astSymbol: AstSymbol = namespaceMemberAstEntity;
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities, entryPoint);
});
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
// (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)
// (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
}
}
}
}
@@ -194,6 +197,7 @@ export class ValidationEnhancer {
collector: Collector,
astDeclaration: AstDeclaration,
alreadyWarnedEntities: Set<AstEntity>,
entryPoint: IWorkingPackageEntryPoint,
): void {
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
const declarationReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
@@ -241,7 +245,7 @@ export class ValidationEnhancer {
);
}
} else {
const entryPointFilename: string = path.basename(collector.workingPackage.entryPointSourceFile.fileName);
const entryPointFilename: string = path.basename(entryPoint!.sourceFile.fileName);
if (!alreadyWarnedEntities.has(referencedEntity)) {
alreadyWarnedEntities.add(referencedEntity);

View File

@@ -51,6 +51,7 @@ import type { AstModule } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { TypeScriptInternals } from '../analyzer/TypeScriptInternals.js';
import type { ExtractorConfig } from '../api/ExtractorConfig';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
@@ -210,6 +211,16 @@ interface IProcessAstEntityContext {
parentDocgenJson?: DocgenContainerJson | undefined;
}
/**
* @beta
*/
export interface IApiModelGenerationOptions {
/**
* The release tags to trim.
*/
releaseTagsToTrim: Set<ReleaseTag>;
}
const linkRegEx =
/{@link\s(?:(?<class>\w+)(?:[#.](?<event>event:)?(?<prop>[\w()]+))?|(?<url>https?:\/\/[^\s}]*))(?<name>\s[^}]*)?}/g;
@@ -239,12 +250,25 @@ export class ApiModelGenerator {
private readonly _referenceGenerator: DeclarationReferenceGenerator;
private readonly _releaseTagsToTrim: Set<ReleaseTag> | undefined;
public readonly docModelEnabled: boolean;
private readonly _jsDocJson: DocgenJson | undefined;
public constructor(collector: Collector) {
public constructor(collector: Collector, extractorConfig: ExtractorConfig) {
this._collector = collector;
this._apiModel = new ApiModel();
this._referenceGenerator = new DeclarationReferenceGenerator(collector);
const apiModelGenerationOptions: IApiModelGenerationOptions | undefined = extractorConfig.docModelGenerationOptions;
if (apiModelGenerationOptions) {
this._releaseTagsToTrim = apiModelGenerationOptions.releaseTagsToTrim;
this.docModelEnabled = true;
} else {
this.docModelEnabled = false;
}
// @ts-expect-error we reuse the private tsdocParser from collector here
this._tsDocParser = collector._tsdocParser;
}
@@ -270,20 +294,22 @@ export class ApiModelGenerator {
});
this._apiModel.addMember(apiPackage);
const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: '' });
apiPackage.addMember(apiEntryPoint);
for (const [entryPoint, entities] of this._collector.entities.entries()) {
const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: entryPoint.modulePath });
apiPackage.addMember(apiEntryPoint);
for (const entity of this._collector.entities) {
// Only process entities that are exported from the entry point. Entities that are exported from
// `AstNamespaceImport` entities will be processed by `_processAstNamespaceImport`. However, if
// we are including forgotten exports, then process everything.
if (entity.exportedFromEntryPoint || this._collector.extractorConfig.docModelIncludeForgottenExports) {
this._processAstEntity(entity.astEntity, {
name: entity.nameForEmit!,
isExported: entity.exportedFromEntryPoint,
parentApiItem: apiEntryPoint,
parentDocgenJson: this._jsDocJson,
});
for (const entity of entities) {
// Only process entities that are exported from the entry point. Entities that are exported from
// `AstNamespaceImport` entities will be processed by `_processAstNamespaceImport`. However, if
// we are including forgotten exports, then process everything.
if (entity.exportedFromEntryPoint || this._collector.extractorConfig.docModelIncludeForgottenExports) {
this._processAstEntity(entity.astEntity, {
name: entity.nameForEmit!,
isExported: entity.exportedFromEntryPoint,
parentApiItem: apiEntryPoint,
parentDocgenJson: this._jsDocJson,
});
}
}
}
@@ -362,8 +388,8 @@ export class ApiModelGenerator {
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
if (releaseTag === ReleaseTag.Internal) {
return; // trim out items marked as "@internal"
if (this._releaseTagsToTrim?.has(releaseTag)) {
return;
}
switch (astDeclaration.declaration.kind) {
@@ -435,15 +461,31 @@ export class ApiModelGenerator {
this._processApiTypeAlias(astDeclaration, context);
break;
case ts.SyntaxKind.VariableDeclaration:
this._processApiVariable(astDeclaration, context);
case ts.SyntaxKind.VariableDeclaration: {
// check for arrow functions in variable declaration
const functionDeclaration: ts.FunctionDeclaration | undefined =
this._tryFindFunctionDeclaration(astDeclaration);
if (functionDeclaration) {
this._processApiFunction(astDeclaration, context, functionDeclaration);
} else {
this._processApiVariable(astDeclaration, context);
}
break;
}
default:
// ignore unknown types
}
}
private _tryFindFunctionDeclaration(astDeclaration: AstDeclaration): ts.FunctionDeclaration | undefined {
const children: readonly ts.Node[] = astDeclaration.declaration.getChildren(
astDeclaration.declaration.getSourceFile(),
);
return children.find(ts.isFunctionTypeNode) as ts.FunctionDeclaration | undefined;
}
private _processChildDeclarations(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
for (const childDeclaration of astDeclaration.children) {
this._processDeclaration(childDeclaration, {
@@ -817,7 +859,11 @@ export class ApiModelGenerator {
}
}
private _processApiFunction(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void {
private _processApiFunction(
astDeclaration: AstDeclaration,
context: IProcessAstEntityContext,
altFunctionDeclaration?: ts.FunctionDeclaration,
): void {
const { name, isExported, parentApiItem } = context;
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
@@ -828,7 +874,8 @@ export class ApiModelGenerator {
const jsDoc = parent?.functions.find((fun) => fun.name === name);
if (apiFunction === undefined) {
const functionDeclaration: ts.FunctionDeclaration = astDeclaration.declaration as ts.FunctionDeclaration;
const functionDeclaration: ts.FunctionDeclaration =
altFunctionDeclaration ?? (astDeclaration.declaration as ts.FunctionDeclaration);
const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
@@ -1799,11 +1846,20 @@ export class ApiModelGenerator {
.flatMap((typ, index) => {
const result = typ.reduce<IExcerptToken[]>((arr, [type, symbol]) => {
const astEntity =
(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);
[...this._collector.entities.values()].reduce<AstSymbol | undefined>(
(found, entities) =>
found ??
(entities.find((entity) => entity.nameForEmit === type && 'astDeclarations' in entity.astEntity)
?.astEntity as AstSymbol | undefined),
undefined,
) ??
[...this._collector.entities.values()].reduce<AstImport | undefined>(
(found, entities) =>
found ??
(entities.find((entity) => entity.nameForEmit === type && 'astSymbol' in entity.astEntity)
?.astEntity as AstImport | undefined),
undefined,
);
const astSymbol = astEntity instanceof AstImport ? astEntity.astSymbol : astEntity;
const match = astEntity instanceof AstImport ? moduleNameRegEx.exec(astEntity.modulePath) : null;
const pkg = match?.groups!.package ?? this._apiModel.packages[0]!.name;

View File

@@ -9,7 +9,7 @@ import * as ts from 'typescript';
import { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js';
@@ -17,12 +17,18 @@ import { Span } from '../analyzer/Span.js';
import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js';
import type { ExtractorMessage } from '../api/ExtractorMessage.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { ApiReportVariant } from '../api/IConfigFile';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import { Collector } from '../collector/Collector.js';
import type { CollectorEntity } from '../collector/CollectorEntity.js';
import type { SymbolMetadata } from '../collector/SymbolMetadata';
import { DtsEmitHelpers } from './DtsEmitHelpers.js';
import { IndentedWriter } from './IndentedWriter.js';
function capitalizeFirstLetter(input: string): string {
return input === '' ? '' : `${input[0]!.toLocaleUpperCase()}${input.slice(1)}`;
}
export class ApiReportGenerator {
private static _trimSpacesRegExp: RegExp = / +$/gm;
@@ -40,208 +46,235 @@ export class ApiReportGenerator {
return normalizedActual === normalizedExpected;
}
public static generateReviewFileContent(collector: Collector): string {
const writer: IndentedWriter = new IndentedWriter();
writer.trimLeadingSpaces = true;
/**
* Generates and returns the API report contents as a string.
*
* @param collector - The collector that has the entities.
* @param reportVariant - The release level with which the report is associated.
* Can also be viewed as the minimal release level of items that should be included in the report.
*/
public static generateReviewFileContent(collector: Collector, reportVariant: ApiReportVariant): Map<string, string> {
// mapping from entrypoint name to its file content
const fileContentMap: Map<string, string> = new Map<string, string>();
writer.writeLine(
[
`## API Report File for "${collector.workingPackage.name}"`,
``,
`> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`,
``,
].join('\n'),
);
for (const [entryPoint, entryPointEntities] of collector.entities) {
const writer: IndentedWriter = new IndentedWriter();
writer.trimLeadingSpaces = true;
// Write the opening delimiter for the Markdown code fence
writer.writeLine('```ts\n');
// For backwards compatibility, don't emit "complete" in report text for untrimmed reports.
const releaseLevelPrefix: string = reportVariant === 'complete' ? '' : `${capitalizeFirstLetter(reportVariant)} `;
writer.writeLine(
[
`## ${releaseLevelPrefix}API Report File for "${collector.workingPackage.name}${entryPoint.modulePath ? '/' : ''}${
entryPoint.modulePath
}"`,
``,
`> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).`,
``,
].join('\n'),
);
// Emit the triple slash directives
for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) {
// https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162
writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`);
}
// Write the opening delimiter for the Markdown code fence
writer.writeLine('```ts\n');
for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) {
writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`);
}
writer.ensureSkippedLine();
// Emit the imports
for (const entity of collector.entities) {
if (entity.astEntity instanceof AstImport) {
DtsEmitHelpers.emitImport(writer, entity, entity.astEntity);
// Emit the triple slash directives
for (const typeDirectiveReference of Array.from(collector.dtsTypeReferenceDirectives).sort()) {
// https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162
writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`);
}
}
writer.ensureSkippedLine();
for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) {
writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`);
}
// Emit the regular declarations
for (const entity of collector.entities) {
const astEntity: AstEntity = entity.astEntity;
if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) {
// First, collect the list of export names for this symbol. When reporting messages with
// ExtractorMessage.properties.exportName, this will enable us to emit the warning comments alongside
// the associated export statement.
interface IExportToEmit {
readonly associatedMessages: ExtractorMessage[];
readonly exportName: string;
writer.ensureSkippedLine();
// Emit the imports
for (const entity of entryPointEntities) {
if (entity.astEntity instanceof AstImport) {
DtsEmitHelpers.emitImport(writer, entity, entity.astEntity);
}
const exportsToEmit: Map<string, IExportToEmit> = new Map<string, IExportToEmit>();
}
for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
writer.ensureSkippedLine();
// Emit the regular declarations
for (const entity of entryPointEntities) {
const astEntity: AstEntity = entity.astEntity;
const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity);
const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata?.maxEffectiveReleaseTag ?? ReleaseTag.None;
if (!this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, reportVariant)) {
continue;
}
if (entity.consumable || collector.extractorConfig.apiReportIncludeForgottenExports) {
// First, collect the list of export names for this symbol. When reporting messages with
// ExtractorMessage.properties.exportName, this will enable us to emit the warning comments alongside
// the associated export statement.
interface IExportToEmit {
readonly associatedMessages: ExtractorMessage[];
readonly exportName: string;
}
}
const exportsToEmit: Map<string, IExportToEmit> = new Map<string, IExportToEmit>();
if (astEntity instanceof AstSymbol) {
// Emit all the declarations for this entity
for (const astDeclaration of astEntity.astDeclarations || []) {
// Get the messages associated with this declaration
const fetchedMessages: ExtractorMessage[] =
collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration);
for (const exportName of entity.exportNames) {
if (!entity.shouldInlineExport) {
exportsToEmit.set(exportName, { exportName, associatedMessages: [] });
}
}
// Peel off the messages associated with an export statement and store them
// in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
// added to messagesToReport, to be emitted next to the declaration instead of the export statement.
const messagesToReport: ExtractorMessage[] = [];
for (const message of fetchedMessages) {
if (message.properties.exportName) {
const exportToEmit: IExportToEmit | undefined = exportsToEmit.get(message.properties.exportName);
if (exportToEmit) {
exportToEmit.associatedMessages.push(message);
continue;
if (astEntity instanceof AstSymbol) {
// Emit all the declarations for this entity
for (const astDeclaration of astEntity.astDeclarations || []) {
// Get the messages associated with this declaration
const fetchedMessages: ExtractorMessage[] =
collector.messageRouter.fetchAssociatedMessagesForReviewFile(astDeclaration);
// Peel off the messages associated with an export statement and store them
// in IExportToEmit.associatedMessages (to be processed later). The remaining messages will
// added to messagesToReport, to be emitted next to the declaration instead of the export statement.
const messagesToReport: ExtractorMessage[] = [];
for (const message of fetchedMessages) {
if (message.properties.exportName) {
const exportToEmit: IExportToEmit | undefined = exportsToEmit.get(message.properties.exportName);
if (exportToEmit) {
exportToEmit.associatedMessages.push(message);
continue;
}
}
messagesToReport.push(message);
}
messagesToReport.push(message);
if (this._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) {
writer.ensureSkippedLine();
writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
const span: Span = new Span(astDeclaration.declaration);
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
if (apiItemMetadata.isPreapproved) {
ApiReportGenerator._modifySpanForPreapproved(span);
} else {
ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false, reportVariant);
}
span.writeModifiedText(writer);
writer.ensureNewLine();
}
}
writer.ensureSkippedLine();
writer.write(ApiReportGenerator._getAedocSynopsis(collector, astDeclaration, messagesToReport));
const span: Span = new Span(astDeclaration.declaration);
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
if (apiItemMetadata.isPreapproved) {
ApiReportGenerator._modifySpanForPreapproved(span);
} else {
ApiReportGenerator._modifySpan(collector, span, entity, astDeclaration, false);
}
span.writeModifiedText(writer);
writer.ensureNewLine();
}
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
if (entity.nameForEmit === undefined) {
// This should never happen
throw new InternalError('referencedEntry.nameForEmit is undefined');
}
if (astModuleExportInfo.starExportedExternalModules.size > 0) {
// We could support this, but we would need to find a way to safely represent it.
throw new Error(
`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` +
SourceFileLocationFormatter.formatDeclaration(astEntity.declaration),
);
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
// Emit a synthetic declaration for the namespace. It will look like this:
//
// declare namespace example {
// export {
// f1,
// f2
// }
// }
//
// Note that we do not try to relocate f1()/f2() to be inside the namespace because other type
// signatures may reference them directly (without using the namespace qualifier).
writer.ensureSkippedLine();
writer.writeLine(`declare namespace ${entity.nameForEmit} {`);
// all local exports of local imported module are just references to top-level declarations
writer.increaseIndent();
writer.writeLine('export {');
writer.increaseIndent();
const exportClauses: string[] = [];
for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) {
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(exportedEntity);
if (collectorEntity === undefined) {
if (entity.nameForEmit === undefined) {
// This should never happen
// top-level exports of local imported module should be added as collector entities before
throw new InternalError(
`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`,
throw new InternalError('referencedEntry.nameForEmit is undefined');
}
if (astModuleExportInfo.starExportedExternalModules.size > 0) {
// We could support this, but we would need to find a way to safely represent it.
throw new Error(
`The ${entity.nameForEmit} namespace import includes a star export, which is not supported:\n` +
SourceFileLocationFormatter.formatDeclaration(astEntity.declaration),
);
}
if (collectorEntity.nameForEmit === exportedName) {
exportClauses.push(collectorEntity.nameForEmit);
} else {
exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`);
}
}
// Emit a synthetic declaration for the namespace. It will look like this:
//
// declare namespace example {
// export {
// f1,
// f2
// }
// }
//
// Note that we do not try to relocate f1()/f2() to be inside the namespace because other type
// signatures may reference them directly (without using the namespace qualifier).
writer.writeLine(exportClauses.join(',\n'));
writer.decreaseIndent();
writer.writeLine('}'); // end of "export { ... }"
writer.decreaseIndent();
writer.writeLine('}'); // end of "declare namespace { ... }"
}
// Now emit the export statements for this entity.
for (const exportToEmit of exportsToEmit.values()) {
// Write any associated messages
if (exportToEmit.associatedMessages.length > 0) {
writer.ensureSkippedLine();
for (const message of exportToEmit.associatedMessages) {
ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
writer.writeLine(`declare namespace ${entity.nameForEmit} {`);
// all local exports of local imported module are just references to top-level declarations
writer.increaseIndent();
writer.writeLine('export {');
writer.increaseIndent();
const exportClauses: string[] = [];
for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) {
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(exportedEntity);
if (collectorEntity === undefined) {
// This should never happen
// top-level exports of local imported module should be added as collector entities before
throw new InternalError(
`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`,
);
}
if (collectorEntity.nameForEmit === exportedName) {
exportClauses.push(collectorEntity.nameForEmit);
} else {
exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`);
}
}
writer.writeLine(exportClauses.join(',\n'));
writer.decreaseIndent();
writer.writeLine('}'); // end of "export { ... }"
writer.decreaseIndent();
writer.writeLine('}'); // end of "declare namespace { ... }"
}
DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity);
// Now emit the export statements for this entity.
for (const exportToEmit of exportsToEmit.values()) {
// Write any associated messages
if (exportToEmit.associatedMessages.length > 0) {
writer.ensureSkippedLine();
for (const message of exportToEmit.associatedMessages) {
ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation());
}
}
DtsEmitHelpers.emitNamedExport(writer, exportToEmit.exportName, entity);
}
writer.ensureSkippedLine();
}
}
DtsEmitHelpers.emitStarExports(writer, collector);
// Write the unassociated warnings at the bottom of the file
const unassociatedMessages: ExtractorMessage[] = collector.messageRouter.fetchUnassociatedMessagesForReviewFile();
if (unassociatedMessages.length > 0) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:');
ApiReportGenerator._writeLineAsComments(writer, '');
for (const unassociatedMessage of unassociatedMessages) {
ApiReportGenerator._writeLineAsComments(
writer,
unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder),
);
}
}
}
DtsEmitHelpers.emitStarExports(writer, collector);
// Write the unassociated warnings at the bottom of the file
const unassociatedMessages: ExtractorMessage[] = collector.messageRouter.fetchUnassociatedMessagesForReviewFile();
if (unassociatedMessages.length > 0) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, 'Warnings were encountered during analysis:');
ApiReportGenerator._writeLineAsComments(writer, '');
for (const unassociatedMessage of unassociatedMessages) {
ApiReportGenerator._writeLineAsComments(
writer,
unassociatedMessage.formatMessageWithLocation(collector.workingPackage.packageFolder),
);
if (collector.workingPackage.tsdocComment === undefined) {
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)');
}
}
if (collector.workingPackage.tsdocComment === undefined) {
// Write the closing delimiter for the Markdown code fence
writer.ensureSkippedLine();
ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)');
writer.writeLine('```');
// Remove any trailing spaces
fileContentMap.set(entryPoint.modulePath, writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, ''));
}
// Write the closing delimiter for the Markdown code fence
writer.ensureSkippedLine();
writer.writeLine('```');
// Remove any trailing spaces
return writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, '');
return fileContentMap;
}
/**
@@ -253,10 +286,11 @@ export class ApiReportGenerator {
entity: CollectorEntity,
astDeclaration: AstDeclaration,
insideTypeLiteral: boolean,
reportVariant: ApiReportVariant,
): void {
// Should we process this declaration at all?
if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) {
if (!ApiReportGenerator._shouldIncludeDeclaration(collector, astDeclaration, reportVariant)) {
span.modification.skipAll();
return;
}
@@ -373,7 +407,14 @@ export class ApiReportGenerator {
case ts.SyntaxKind.ImportType:
DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => {
ApiReportGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, insideTypeLiteral);
ApiReportGenerator._modifySpan(
collector,
childSpan,
entity,
childAstDeclaration,
insideTypeLiteral,
reportVariant,
);
});
break;
@@ -408,11 +449,56 @@ export class ApiReportGenerator {
}
}
ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral);
ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral, reportVariant);
}
}
}
private static _shouldIncludeDeclaration(
collector: Collector,
astDeclaration: AstDeclaration,
reportVariant: ApiReportVariant,
): boolean {
// Private declarations are not included in the API report
if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) {
return false;
}
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
return this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, reportVariant);
}
private static _shouldIncludeReleaseTag(releaseTag: ReleaseTag, reportVariant: ApiReportVariant): boolean {
switch (reportVariant) {
case 'complete':
return true;
case 'alpha':
return (
releaseTag === ReleaseTag.Alpha ||
releaseTag === ReleaseTag.Beta ||
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
case 'beta':
return (
releaseTag === ReleaseTag.Beta ||
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
case 'public':
return (
releaseTag === ReleaseTag.Public ||
// NOTE: No specified release tag is implicitly treated as `@public`.
releaseTag === ReleaseTag.None
);
default:
throw new Error(`Unrecognized release level: ${reportVariant}`);
}
}
/**
* For declarations marked as `@preapproved`, this is used instead of _modifySpan().
*/
@@ -482,30 +568,59 @@ export class ApiReportGenerator {
if (!collector.isAncillaryDeclaration(astDeclaration)) {
const footerParts: string[] = [];
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
// 1. Release tag (if present)
if (!apiItemMetadata.releaseTagSameAsParent && apiItemMetadata.effectiveReleaseTag !== ReleaseTag.None) {
footerParts.push(releaseTagGetTagName(apiItemMetadata.effectiveReleaseTag));
}
if (apiItemMetadata.isSealed) {
// 2. Enumerate configured tags, reporting standard system tags first and then other configured tags.
// Note that the ordering we handle the standard tags is important for backwards compatibility.
// Also note that we had special mechanisms for checking whether or not an item is documented with these tags,
// so they are checked specially.
const {
'@sealed': reportSealedTag,
'@virtual': reportVirtualTag,
'@override': reportOverrideTag,
'@eventProperty': reportEventPropertyTag,
'@deprecated': reportDeprecatedTag,
...otherTagsToReport
} = collector.extractorConfig.tagsToReport;
// 2.a Check for standard tags and report those that are both configured and present in the metadata.
if (reportSealedTag && apiItemMetadata.isSealed) {
footerParts.push('@sealed');
}
if (apiItemMetadata.isVirtual) {
if (reportVirtualTag && apiItemMetadata.isVirtual) {
footerParts.push('@virtual');
}
if (apiItemMetadata.isOverride) {
if (reportOverrideTag && apiItemMetadata.isOverride) {
footerParts.push('@override');
}
if (apiItemMetadata.isEventProperty) {
if (reportEventPropertyTag && apiItemMetadata.isEventProperty) {
footerParts.push('@eventProperty');
}
if (apiItemMetadata.tsdocComment?.deprecatedBlock) {
if (reportDeprecatedTag && apiItemMetadata.tsdocComment?.deprecatedBlock) {
footerParts.push('@deprecated');
}
// 2.b Check for other configured tags and report those that are present in the tsdoc metadata.
for (const [tag, reportTag] of Object.entries(otherTagsToReport)) {
// If the tag was not handled specially, check if it is present in the metadata.
if (
reportTag &&
(apiItemMetadata.tsdocComment?.customBlocks.some((block) => block.blockTag.tagName === tag) ||
apiItemMetadata.tsdocComment?.modifierTagSet.hasTagName(tag))
) {
footerParts.push(tag);
}
}
// 3. If the item is undocumented, append notice at the end of the list
if (apiItemMetadata.undocumented) {
footerParts.push('(undocumented)');

View File

@@ -341,7 +341,7 @@ export class DeclarationReferenceGenerator {
}
}
private _getPackageName(sourceFile: ts.SourceFile): string {
private _getEntryPointName(sourceFile: ts.SourceFile): string {
if (this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) {
const packageJson: INodePackageJson | undefined = this._collector.packageJsonLookup.tryLoadNodePackageJsonFor(
sourceFile.fileName,
@@ -354,18 +354,25 @@ export class DeclarationReferenceGenerator {
return DeclarationReferenceGenerator.unknownReference;
}
return this._collector.workingPackage.name;
let modulePath = '';
for (const entryPoint of this._collector.workingPackage.entryPoints) {
if (entryPoint.sourceFile === sourceFile) {
modulePath = entryPoint.modulePath;
}
}
return `${this._collector.workingPackage.name}${modulePath ? `/${modulePath}` : ''}`;
}
private _sourceFileToModuleSource(sourceFile: ts.SourceFile | undefined): GlobalSource | ModuleSource {
if (sourceFile && ts.isExternalModule(sourceFile)) {
const packageName: string = this._getPackageName(sourceFile);
const packageName: string = this._getEntryPointName(sourceFile);
if (this._collector.bundledPackageNames.has(packageName)) {
// The api-extractor.json config file has a "bundledPackages" setting, which causes imports from
// certain NPM packages to be treated as part of the working project. In this case, we need to
// substitute the working package name.
return new ModuleSource(this._collector.workingPackage.name);
return new ModuleSource(this._collector.workingPackage.name); // TODO: make it work with multiple entrypoints
} else {
return new ModuleSource(packageName);
}

View File

@@ -8,7 +8,7 @@ import * as ts from 'typescript';
import { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import { AstImport } from '../analyzer/AstImport.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import type { IAstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js';
@@ -103,8 +103,10 @@ export class DtsRollupGenerator {
writer.ensureSkippedLine();
// dtsRollup doesn't support multiple entry points. We throw error if dtsRollup is enabled while more than one entry points are specified.
// So at this point, we can safely assume there is only one entry point in collector.entities
// Emit the imports
for (const entity of collector.entities) {
for (const entity of [...collector.entities.values()][0]!) {
if (entity.astEntity instanceof AstImport) {
const astImport: AstImport = entity.astEntity;
@@ -124,7 +126,7 @@ export class DtsRollupGenerator {
writer.ensureSkippedLine();
// Emit the regular declarations
for (const entity of collector.entities) {
for (const entity of [...collector.entities.values()][0]!) {
const astEntity: AstEntity = entity.astEntity;
const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity);
const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata
@@ -159,7 +161,7 @@ export class DtsRollupGenerator {
}
if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
const astModuleExportInfo: IAstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);
if (entity.nameForEmit === undefined) {
// This should never happen

View File

@@ -1,119 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { IndentedWriter } from '../IndentedWriter.js';
test('01 Demo from docs', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.write('begin\n');
indentedWriter.increaseIndent();
indentedWriter.write('one\ntwo\n');
indentedWriter.decreaseIndent();
indentedWriter.increaseIndent();
indentedWriter.decreaseIndent();
indentedWriter.write('end');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('02 Indent something', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.write('a');
indentedWriter.write('b');
indentedWriter.increaseIndent();
indentedWriter.writeLine('c');
indentedWriter.writeLine('d');
indentedWriter.decreaseIndent();
indentedWriter.writeLine('e');
indentedWriter.increaseIndent('>>> ');
indentedWriter.writeLine();
indentedWriter.writeLine();
indentedWriter.writeLine('g');
indentedWriter.decreaseIndent();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('03 Indent something with indentBlankLines=true', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.indentBlankLines = true;
indentedWriter.write('a');
indentedWriter.write('b');
indentedWriter.increaseIndent();
indentedWriter.writeLine('c');
indentedWriter.writeLine('d');
indentedWriter.decreaseIndent();
indentedWriter.writeLine('e');
indentedWriter.increaseIndent('>>> ');
indentedWriter.writeLine();
indentedWriter.writeLine();
indentedWriter.writeLine('g');
indentedWriter.decreaseIndent();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('04 Two kinds of indents', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.writeLine('---');
indentedWriter.indentScope(() => {
indentedWriter.write('a\nb');
indentedWriter.indentScope(() => {
indentedWriter.write('c\nd\n');
});
indentedWriter.write('e\n');
}, '> ');
indentedWriter.writeLine('---');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('05 Edge cases for ensureNewLine()', () => {
let indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.ensureNewLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
indentedWriter = new IndentedWriter();
indentedWriter.write('previous');
indentedWriter.ensureNewLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('06 Edge cases for ensureSkippedLine()', () => {
let indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.ensureSkippedLine();
indentedWriter.write('line');
expect(indentedWriter.toString()).toMatchSnapshot();
indentedWriter = new IndentedWriter();
indentedWriter.write('previous');
indentedWriter.ensureSkippedLine();
indentedWriter.write('line');
indentedWriter.ensureSkippedLine();
expect(indentedWriter.toString()).toMatchSnapshot();
});
test('06 trimLeadingSpaces=true', () => {
const indentedWriter: IndentedWriter = new IndentedWriter();
indentedWriter.trimLeadingSpaces = true;
// Example from doc comment
indentedWriter.increaseIndent(' ');
indentedWriter.write(' a\n b c\n');
indentedWriter.decreaseIndent();
indentedWriter.ensureSkippedLine();
indentedWriter.increaseIndent('>>');
indentedWriter.write(' ');
indentedWriter.write(' ');
indentedWriter.write(' a');
indentedWriter.writeLine(' b');
indentedWriter.writeLine('\ttab'); // does not get indented
indentedWriter.writeLine('c ');
expect(indentedWriter.toString()).toMatchSnapshot();
});

View File

@@ -1,65 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`01 Demo from docs 1`] = `
"begin
one
two
end"
`;
exports[`02 Indent something 1`] = `
"abc
d
e
>>> g
"
`;
exports[`03 Indent something with indentBlankLines=true 1`] = `
"abc
d
e
>>>
>>>
>>> g
"
`;
exports[`04 Two kinds of indents 1`] = `
"---
> a
> bc
> d
> e
---
"
`;
exports[`05 Edge cases for ensureNewLine() 1`] = `"line"`;
exports[`05 Edge cases for ensureNewLine() 2`] = `
"previous
line"
`;
exports[`06 Edge cases for ensureSkippedLine() 1`] = `"line"`;
exports[`06 Edge cases for ensureSkippedLine() 2`] = `
"previous
line
"
`;
exports[`06 trimLeadingSpaces=true 1`] = `
" a
b c
>>a b
>> tab
>>c
"
`;

View File

@@ -138,15 +138,31 @@
"enabled": true
/**
* The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce
* a full file path.
* The base filename for the API report files, to be combined with "reportFolder" or "reportTempFolder"
* to produce the full file path. The "reportFileName" should not include any path separators such as
* "\" or "/". The "reportFileName" should not include a file extension, since API Extractor will automatically
* append an appropriate file extension such as ".api.md". If the "reportVariants" setting is used, then the
* file extension includes the variant name, for example "my-report.public.api.md" or "my-report.beta.api.md".
* The "complete" variant always uses the simple extension "my-report.api.md".
*
* The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
* Previous versions of API Extractor required "reportFileName" to include the ".api.md" extension explicitly;
* for backwards compatibility, that is still accepted but will be discarded before applying the above rules.
*
* SUPPORTED TOKENS: <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<unscopedPackageName>.api.md"
* DEFAULT VALUE: "<unscopedPackageName>"
*/
// "reportFileName": "<unscopedPackageName>.api.md",
// "reportFileName": "<unscopedPackageName>",
/**
* To support different approval requirements for different API levels, multiple "variants" of the API report can
* be generated. The "reportVariants" setting specifies a list of variants to be generated. If omitted,
* by default only the "complete" variant will be generated, which includes all @internal, @alpha, @beta,
* and @public items. Other possible variants are "alpha" (@alpha + @beta + @public), "beta" (@beta + @public),
* and "public" (@public only).
*
* DEFAULT VALUE: [ "complete" ]
*/
// "reportVariants": ["public", "beta"],
/**
* Specifies the folder where the API report file is written. The file name portion is determined by
@@ -159,9 +175,9 @@
* prepend a folder token such as "<projectFolder>".
*
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/"
* DEFAULT VALUE: "<projectFolder>/etc/"
*/
// "reportFolder": "<projectFolder>/temp/",
// "reportFolder": "<projectFolder>/etc/",
/**
* Specifies the folder where the temporary report file is written. The file name portion is determined by
@@ -226,7 +242,7 @@
* item's file path is "api/ExtractorConfig.ts", the full URL file path would be
* "https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js".
*
* Can be omitted if you don't need source code links in your API documentation reference.
* This setting can be omitted if you don't need source code links in your API documentation reference.
*
* SUPPORTED TOKENS: none
* DEFAULT VALUE: ""
@@ -261,6 +277,8 @@
* Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release.
* This file will include only declarations that are marked as "@public", "@beta", or "@alpha".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*
@@ -273,6 +291,8 @@
* Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
* This file will include only declarations that are marked as "@public" or "@beta".
*
* If the path is an empty string, then this file will not be written.
*
* The path is resolved relative to the folder of the config file that contains the setting; to change this,
* prepend a folder token such as "<projectFolder>".
*

View File

@@ -23,6 +23,14 @@
"type": "string"
},
"additionalEntryPoints": {
"description": "Specifies the .d.ts files to be used as the starting points for analysis.",
"type": "array",
"items": {
"type": "object"
}
},
"bundledPackages": {
"description": "A list of NPM package names whose exports should be treated as part of this package.",
"type": "array",
@@ -68,8 +76,17 @@
},
"reportFileName": {
"description": "The filename for the API report files. It will be combined with \"reportFolder\" or \"reportTempFolder\" to produce a full file path. The file extension should be \".api.md\", and the string should not contain a path separator such as \"\\\" or \"/\".",
"type": "string"
"description": "The base filename for the API report files, to be combined with \"reportFolder\" or \"reportTempFolder\" to produce the full file path. The \"reportFileName\" should not include any path separators such as \"\\\" or \"/\". The \"reportFileName\" should not include a file extension, since API Extractor will automatically append an appropriate file extension such as \".api.md\". If the \"reportVariants\" setting is used, then the file extension includes the variant name, for example \"my-report.public.api.md\" or \"my-report.beta.api.md\". The \"complete\" variant always uses the simple extension \"my-report.api.md\".\n\nPrevious versions of API Extractor required \"reportFileName\" to include the \".api.md\" extension explicitly; for backwards compatibility, that is still accepted but will be discarded before applying the above rules.",
"type": ["string"]
},
"reportVariants": {
"description": "To support different approval requirements for different API levels, multiple \"variants\" of the API report can be generated. The \"reportVariants\" setting specifies a list of variants to be generated. If omitted, by default only the \"complete\" variant will be generated, which includes all @internal, @alpha, @beta, and @public items. Other possible variants are \"alpha\" (@alpha + @beta + @public), \"beta\" (@beta + @public), and \"public\" (@public only).",
"type": "array",
"items": {
"type": "string",
"enum": ["public", "beta", "alpha", "complete"]
}
},
"reportFolder": {
@@ -85,6 +102,17 @@
"includeForgottenExports": {
"description": "Whether \"forgotten exports\" should be included in the API report file. Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.",
"type": "boolean"
},
"tagsToReport": {
"description": "Specifies a list of TSDoc tags that should be reported in the API report file for items whose documentation contains them. This can be used to include standard TSDoc tags or custom ones. Specified tag names must begin with \"@\". By default, the following tags are reported: [@sealed, @virtual, @override, @eventProperty, @deprecated]. Tags will appear in the order they are specified in this list. Note that an item's release tag will always reported; this behavior cannot be overridden.",
"type": "object",
"patternProperties": {
"^@[^\\s]*$": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"required": ["enabled"],
@@ -110,6 +138,14 @@
"projectFolderUrl": {
"description": "The base URL where the project's source code can be viewed on a website such as GitHub or Azure DevOps. This URL path corresponds to the `<projectFolder>` path on disk. This URL is concatenated with the file paths serialized to the doc model to produce URL file paths to individual API items. For example, if the `projectFolderUrl` is \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor\" and an API item's file path is \"api/ExtractorConfig.ts\", the full URL file path would be \"https://github.com/microsoft/rushstack/tree/main/apps/api-extractor/api/ExtractorConfig.js\". Can be omitted if you don't need source code links in your API documentation reference.",
"type": "string"
},
"releaseTagsToTrim": {
"description": "Specifies a list of release tags that will be trimmed from the doc model. The default value is `[\"@internal\"]`.",
"type": "array",
"items": {
"enum": ["@internal", "@alpha", "@beta", "@public"]
},
"uniqueItems": true
}
},
"required": ["enabled"],

View File

@@ -14,7 +14,7 @@ console.log(
const parser: ApiExtractorCommandLine = new ApiExtractorCommandLine();
// eslint-disable-next-line promise/prefer-await-to-callbacks
parser.execute().catch((error) => {
parser.executeAsync().catch((error) => {
console.error(colors.red(`An unexpected error occurred:`), error);
process.exit(1);
});

View File

@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"types": ["node"],
"isolatedModules": false,
"outDir": "./dist",
"esModuleInterop": true

View File

@@ -87,7 +87,7 @@
"@discordjs/scripts": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@types/node": "^22.15.2",
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"cross-env": "^7.0.3",
"dtslint": "4.2.1",

View File

@@ -5,7 +5,7 @@
"toolPackages": [
{
"packageName": "@discordjs/api-extractor",
"packageVersion": "7.38.1"
"packageVersion": "7.52.7"
}
]
}

View File

@@ -2,5 +2,25 @@
"extends": "../../api-extractor.json",
"docModel": {
"projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/next"
}
},
"additionalEntryPoints": [
{ "modulePath": "discord-api-types", "filePath": "<projectFolder>/dist-docs/exports/discord-api-types.d.ts" },
{ "modulePath": "builders", "filePath": "<projectFolder>/dist-docs/exports/builders.d.ts" },
{ "modulePath": "collection", "filePath": "<projectFolder>/dist-docs/exports/collection.d.ts" },
{ "modulePath": "core", "filePath": "<projectFolder>/dist-docs/exports/core.d.ts" },
{ "modulePath": "formatters", "filePath": "<projectFolder>/dist-docs/exports/formatters.d.ts" },
{ "modulePath": "rest", "filePath": "<projectFolder>/dist-docs/exports/rest.d.ts" },
{ "modulePath": "util", "filePath": "<projectFolder>/dist-docs/exports/util.d.ts" },
{ "modulePath": "ws", "filePath": "<projectFolder>/dist-docs/exports/ws.d.ts" }
],
"bundledPackages": [
"discord-api-types",
"@discordjs/builders",
"@discordjs/collection",
"@discordjs/core",
"@discordjs/formatters",
"@discordjs/rest",
"@discordjs/util",
"@discordjs/ws"
]
}

View File

@@ -63,8 +63,8 @@
"@actions/glob": "^0.5.0",
"@discordjs/api-extractor-model": "workspace:^",
"@discordjs/api-extractor-utils": "workspace:^",
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@microsoft/tsdoc": "~0.15.1",
"@microsoft/tsdoc-config": "~0.17.1",
"@vercel/blob": "^0.27.3",
"@vercel/postgres": "^0.9.0",
"commander": "^13.1.0",

View File

@@ -1,26 +1,24 @@
import { mkdir, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { cwd } from 'node:process';
import type {
ApiClass,
ApiConstructor,
ApiDeclaredItem,
ApiDocumentedItem,
ApiEntryPoint,
ApiEnum,
ApiEnumMember,
ApiEvent,
ApiInterface,
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
ApiProperty,
ApiPropertySignature,
ApiTypeAlias,
ApiVariable,
} from '@discordjs/api-extractor-model';
import {
type ApiClass,
type ApiConstructor,
type ApiDeclaredItem,
type ApiDocumentedItem,
type ApiEntryPoint,
type ApiEnum,
type ApiEnumMember,
type ApiEvent,
type ApiInterface,
type ApiItem,
type ApiItemContainerMixin,
type ApiMethod,
type ApiMethodSignature,
type ApiProperty,
type ApiPropertySignature,
type ApiTypeAlias,
type ApiVariable,
ApiTypeParameterListMixin,
Excerpt,
Meaning,
@@ -52,22 +50,16 @@ import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/De
import { BuiltinDocumentationLinks } from './builtinDocumentationLinks.js';
import { PACKAGES, fetchVersionDocs, fetchVersions } from './shared.js';
function resolvePackageName(packageName: string) {
return packageName === 'discord.js' ? packageName : `@discordjs/${packageName}`;
function findMemberByKey(entry: ApiEntryPoint, containerKey: string) {
return entry.tryGetMemberByKey(containerKey);
}
function findMemberByKey(model: ApiModel, packageName: string, containerKey: string) {
const pkg = model.tryGetPackageByName(resolvePackageName(packageName))!;
return (pkg.members[0] as ApiEntryPoint).tryGetMemberByKey(containerKey);
}
function findMember(model: ApiModel, packageName: string, memberName: string | undefined) {
function findMember(entry: ApiEntryPoint, memberName: string | undefined) {
if (!memberName) {
return undefined;
}
const pkg = model.tryGetPackageByName(resolvePackageName(packageName))!;
return pkg.entryPoints[0]?.findMembersByName(memberName)[0];
return entry.findMembersByName(memberName)[0];
}
/**
@@ -215,15 +207,19 @@ export function hasEvents(item: ApiItemContainerMixin) {
interface ApiItemLike {
containerKey?: string;
displayName: string;
getAssociatedEntryPoint?(): ApiEntryPoint | undefined;
kind: string;
members?: readonly ApiItemLike[];
parent?: ApiItemLike | undefined;
}
function resolveItemURI(item: ApiItemLike): string {
return !item.parent || item.parent.kind === ApiItemKind.EntryPoint
? `${item.displayName}:${item.kind}`
: `${item.parent.displayName}:${item.parent.kind}#${item.displayName}`;
function resolveItemURI(item: ApiItemLike, entryPoint?: ApiEntryPoint): string {
const actualEntryPoint = entryPoint ?? item.getAssociatedEntryPoint?.();
return `${actualEntryPoint?.importPath ? `${actualEntryPoint.importPath}/` : ''}${
!item.parent || item.parent.kind === ApiItemKind.EntryPoint
? `${item.displayName}:${item.kind}`
: `${item.parent.displayName}:${item.parent.kind}#${item.displayName}`
}`;
}
function itemExcerptText(excerpt: Excerpt, apiPackage: ApiPackage, parent?: ApiTypeParameterListMixin) {
@@ -1040,11 +1036,13 @@ function memberKind(member: ApiItem | null) {
}
async function writeSplitDocsToFileSystem({
entry,
member,
packageName,
tag = 'main',
overrideName,
}: {
entry?: string;
member: Record<string, any>;
overrideName?: string;
packageName: string;
@@ -1064,7 +1062,7 @@ async function writeSplitDocsToFileSystem({
'docs',
packageName,
dir,
`${tag}.${overrideName ?? `${member.displayName.toLowerCase()}.${member.kind.toLowerCase()}`}.api.json`,
`${tag}.${entry ? `${entry.replaceAll('/', '.')}.` : ''}${overrideName ?? `${member.displayName.toLowerCase()}.${member.kind.toLowerCase()}`}.api.json`,
),
JSON.stringify(member),
);
@@ -1082,9 +1080,9 @@ export async function generateSplitDocumentation({
const model = new ApiModel();
model.addMember(ApiPackage.loadFromJson(data));
const pkg = model.tryGetPackageByName(pkgName);
const entry = pkg?.entryPoints[0];
const entries = pkg?.entryPoints;
if (!entry) {
if (!entries) {
continue;
}
@@ -1095,52 +1093,70 @@ export async function generateSplitDocumentation({
overrideName: 'dependencies',
});
const members = entry.members
.filter((item) => {
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (item.kind) {
case ApiItemKind.Function:
return (item as ApiFunction).overloadIndex === 1;
case ApiItemKind.Interface:
return !entry.members.some(
(innerItem) => innerItem.kind === ApiItemKind.Class && innerItem.displayName === item.displayName,
);
default:
return true;
}
})
.map((item) => ({
kind: item.kind,
name: item.displayName,
href: resolveItemURI(item),
}));
await writeSplitDocsToFileSystem({
member: members,
member: entries.map((entry) => ({
entryPoint: entry.importPath,
})),
packageName: pkgName,
tag: version,
overrideName: 'sitemap',
overrideName: 'entrypoints',
});
for (const member of members) {
const item = `${member.name}:${member.kind}`;
const [memberName, overloadIndex] = decodeURIComponent(item).split(':');
for (const entry of entries) {
const members = entry.members
.filter((item) => {
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (item.kind) {
case ApiItemKind.Function:
return (item as ApiFunction).overloadIndex === 1;
case ApiItemKind.Interface:
return !entry.members.some(
(innerItem) => innerItem.kind === ApiItemKind.Class && innerItem.displayName === item.displayName,
);
default:
return true;
}
})
.map((item) => ({
kind: item.kind,
name: item.displayName,
href: resolveItemURI(item),
entry: entry.importPath,
}));
// eslint-disable-next-line prefer-const
let { containerKey, displayName: name } = findMember(model, pkgName, memberName) ?? {};
if (name && overloadIndex && !Number.isNaN(Number.parseInt(overloadIndex, 10))) {
containerKey = ApiFunction.getContainerKey(name, Number.parseInt(overloadIndex, 10));
await writeSplitDocsToFileSystem({
member: members,
packageName: pkgName,
tag: version,
overrideName: 'sitemap',
entry: entry.importPath,
});
for (const member of members) {
const item = `${member.name}:${member.kind}`;
const [memberName, overloadIndex] = decodeURIComponent(item).split(':');
// eslint-disable-next-line prefer-const
let { containerKey, displayName: name } = findMember(entry, memberName) ?? {};
if (name && overloadIndex && !Number.isNaN(Number.parseInt(overloadIndex, 10))) {
containerKey = ApiFunction.getContainerKey(name, Number.parseInt(overloadIndex, 10));
}
const foundMember = memberName && containerKey ? (findMemberByKey(entry, containerKey) ?? null) : null;
const returnValue = memberKind(foundMember);
if (!returnValue) {
continue;
}
await writeSplitDocsToFileSystem({
member: returnValue,
packageName: pkgName,
tag: version,
entry: entry.importPath,
});
}
const foundMember = memberName && containerKey ? (findMemberByKey(model, pkgName, containerKey) ?? null) : null;
const returnValue = memberKind(foundMember);
if (!returnValue) {
continue;
}
await writeSplitDocsToFileSystem({ member: returnValue, packageName: pkgName, tag: version });
}
}
}

View File

@@ -13,6 +13,7 @@ export const PACKAGES = [
'util',
'voice',
'ws',
'discord-api-types',
];
export async function fetchVersions(pkg: string) {

View File

@@ -1,3 +1,4 @@
import type { Awaitable } from '@discordjs/util';
import type { IIdentifyThrottler } from '../../throttling/IIdentifyThrottler.js';
import type { SessionInfo, WebSocketManager } from '../../ws/WebSocketManager.js';
import type { FetchingStrategyOptions, IContextFetchingStrategy } from './IContextFetchingStrategy.js';
@@ -28,7 +29,7 @@ export class SimpleContextFetchingStrategy implements IContextFetchingStrategy {
return this.manager.options.retrieveSessionInfo(shardId);
}
public updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null) {
public updateSessionInfo(shardId: number, sessionInfo: SessionInfo | null): Awaitable<void> {
return this.manager.options.updateSessionInfo(shardId, sessionInfo);
}

View File

@@ -1,3 +1,4 @@
import type { Collection } from '@discordjs/collection';
import type { REST } from '@discordjs/rest';
import { range, type Awaitable } from '@discordjs/util';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
@@ -15,7 +16,7 @@ import {
import type { IShardingStrategy } from '../strategies/sharding/IShardingStrategy.js';
import type { IIdentifyThrottler } from '../throttling/IIdentifyThrottler.js';
import { DefaultWebSocketManagerOptions, type CompressionMethod, type Encoding } from '../utils/constants.js';
import type { WebSocketShardDestroyOptions, WebSocketShardEvents } from './WebSocketShard.js';
import type { WebSocketShardDestroyOptions, WebSocketShardEvents, WebSocketShardStatus } from './WebSocketShard.js';
/**
* Represents a range of shard ids
@@ -400,7 +401,7 @@ export class WebSocketManager extends AsyncEventEmitter<ManagerShardEventsMap> i
return this.strategy.send(shardId, payload);
}
public fetchStatus() {
public fetchStatus(): Awaitable<Collection<number, WebSocketShardStatus>> {
return this.strategy.fetchStatus();
}

View File

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

498
pnpm-lock.yaml generated
View File

@@ -5,9 +5,9 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
'@microsoft/tsdoc-config@0.16.2':
hash: cda37396c30a2865185c82c3ac8d7d5a0b1c5eebab1dbca7a3c29e3c17d96247
path: patches/@microsoft__tsdoc-config@0.16.2.patch
'@microsoft/tsdoc-config@0.17.1':
hash: 3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8
path: patches/@microsoft__tsdoc-config@0.17.1.patch
importers:
@@ -268,7 +268,7 @@ importers:
version: 4.1.0(react@19.1.0)
'@vercel/analytics':
specifier: ^1.5.0
version: 1.5.0(next@15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
version: 1.5.0(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
'@vercel/edge-config':
specifier: ^1.4.0
version: 1.4.0
@@ -283,7 +283,7 @@ importers:
version: 1.0.0-beta.3(typescript@5.8.3)
geist:
specifier: ^1.3.1
version: 1.3.1(next@15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
version: 1.3.1(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
immer:
specifier: ^10.1.1
version: 10.1.1
@@ -303,8 +303,8 @@ importers:
specifier: ^12.9.2
version: 12.9.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next:
specifier: 15.4.0-canary.11
version: 15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: 15.4.0-canary.31
version: 15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
next-mdx-remote-client:
specifier: ^2.1.1
version: 2.1.1(@types/react@19.1.2)(acorn@8.14.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -313,7 +313,7 @@ importers:
version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nuqs:
specifier: ^2.4.3
version: 2.4.3(next@15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
version: 2.4.3(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
overlayscrollbars:
specifier: ^2.11.1
version: 2.11.1
@@ -515,20 +515,17 @@ importers:
specifier: workspace:^
version: link:../api-extractor-model
'@microsoft/tsdoc':
specifier: 0.14.2
version: 0.14.2
specifier: ~0.15.1
version: 0.15.1
'@microsoft/tsdoc-config':
specifier: 0.16.2
version: 0.16.2(patch_hash=cda37396c30a2865185c82c3ac8d7d5a0b1c5eebab1dbca7a3c29e3c17d96247)
specifier: ~0.17.1
version: 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library':
specifier: 4.1.0
version: 4.1.0(@types/node@22.15.2)
'@rushstack/rig-package':
specifier: 0.5.3
version: 0.5.3
specifier: 5.13.1
version: 5.13.1(@types/node@22.15.2)
'@rushstack/ts-command-line':
specifier: 4.17.1
version: 4.17.1
specifier: 5.0.1
version: 5.0.1(@types/node@22.15.2)
colors:
specifier: ~1.4.0
version: 1.4.0
@@ -542,15 +539,12 @@ importers:
specifier: ~7.6.3
version: 7.6.3
source-map:
specifier: 0.6.1
specifier: ~0.6.1
version: 0.6.1
typescript:
specifier: ~5.5.4
version: 5.5.4
devDependencies:
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
'@types/lodash':
specifier: ^4.17.16
version: 4.17.16
@@ -581,9 +575,6 @@ importers:
eslint-formatter-pretty:
specifier: ^6.0.1
version: 6.0.1
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -600,18 +591,15 @@ importers:
packages/api-extractor-model:
dependencies:
'@microsoft/tsdoc':
specifier: 0.14.2
version: 0.14.2
specifier: ~0.15.1
version: 0.15.1
'@microsoft/tsdoc-config':
specifier: 0.16.2
version: 0.16.2(patch_hash=cda37396c30a2865185c82c3ac8d7d5a0b1c5eebab1dbca7a3c29e3c17d96247)
specifier: ~0.17.1
version: 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library':
specifier: 4.1.0
version: 4.1.0(@types/node@22.15.2)
specifier: 5.13.1
version: 5.13.1(@types/node@22.15.2)
devDependencies:
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
'@types/node':
specifier: ^22.15.2
version: 22.15.2
@@ -630,9 +618,6 @@ importers:
eslint-formatter-pretty:
specifier: ^6.0.1
version: 6.0.1
jest:
specifier: ^29.7.0
version: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -655,8 +640,8 @@ importers:
specifier: workspace:^
version: link:../api-extractor-model
'@microsoft/tsdoc':
specifier: 0.14.2
version: 0.14.2
specifier: ~0.15.1
version: 0.15.1
devDependencies:
'@types/node':
specifier: ^22.15.2
@@ -1066,7 +1051,7 @@ importers:
specifier: ^22.15.2
version: 22.15.2
'@typescript-eslint/eslint-plugin':
specifier: ^8.31.0
specifier: ^8.29.0
version: 8.31.0(@typescript-eslint/parser@8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser':
specifier: ^8.29.0
@@ -1504,11 +1489,11 @@ importers:
specifier: workspace:^
version: link:../api-extractor-utils
'@microsoft/tsdoc':
specifier: 0.14.2
version: 0.14.2
specifier: ~0.15.1
version: 0.15.1
'@microsoft/tsdoc-config':
specifier: 0.16.2
version: 0.16.2(patch_hash=cda37396c30a2865185c82c3ac8d7d5a0b1c5eebab1dbca7a3c29e3c17d96247)
specifier: ~0.17.1
version: 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@vercel/blob':
specifier: ^0.27.3
version: 0.27.3
@@ -3533,15 +3518,9 @@ packages:
resolution: {integrity: sha512-QEs6l8h7p9eOSHrQ9NBBUZhUuq+j/2QKcRgigbSs2YQepKz8glvsqmsUOp+nvuaY60ps7KkpVVYQCj81WLoMVQ==}
hasBin: true
'@microsoft/tsdoc-config@0.16.2':
resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==}
'@microsoft/tsdoc-config@0.17.1':
resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==}
'@microsoft/tsdoc@0.14.2':
resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==}
'@microsoft/tsdoc@0.15.1':
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
@@ -3561,6 +3540,9 @@ packages:
'@next/env@15.4.0-canary.11':
resolution: {integrity: sha512-UoGIxI4VR0QxCCkVLKw4USofRR72BdwwqbJZKKA9W812mWoO7gMDAkSQ2pARul+0/LPmzzWQGzV4MNJykPw7sg==}
'@next/env@15.4.0-canary.31':
resolution: {integrity: sha512-v9qjCjWhJOcBKVLzsx00JlraSFJPd4XwY5qaIaIfslpWS7qtWrwU01IOSLGBkM+JwAa/VWoVeT5p/dqqbuJgKw==}
'@next/eslint-plugin-next@15.2.4':
resolution: {integrity: sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==}
@@ -3570,48 +3552,96 @@ packages:
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-arm64@15.4.0-canary.31':
resolution: {integrity: sha512-zBo+HeZm3IVlwHGPQpTBfdlYQrcQ4PHkuhL4/s/drVaYrFccL5bf5px2xNL3s6gje+RQm7AnJeJdWK7dXcF1LQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.4.0-canary.11':
resolution: {integrity: sha512-Zz0sUNNRpeCNXsfZDHo2gSL3d2ln2rPR6pRCoif9bBtRRU8z25ernDys1RR7NqvMhYKlJSaOsK/eQeQERLY+ow==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-darwin-x64@15.4.0-canary.31':
resolution: {integrity: sha512-GxZK9xddHRJB4oXmxd0gY+EKpmeW2ioA8b0fmsVrFgaLD+BlKol4z+6UgjtlaVkxXGX8gqd4kvoZ6jNKkGKfNw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.4.0-canary.11':
resolution: {integrity: sha512-hKsDUWFwgmBUj3PESle/KiOZ3lDELIHY9AbEsEkkUv6ddxTmDWurxz17ARUihUtWIewevwtDms0X6/tGnwMHsQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-gnu@15.4.0-canary.31':
resolution: {integrity: sha512-8ZJUYhpc3uOXCm8c38qRCW5OdRgLczMxhyMITUpbQrYZ+csncvaTkeAjUEZ0OyZqrq+5LqSs3qbaZZrd8wQMYw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.4.0-canary.11':
resolution: {integrity: sha512-HqICfZyXDX+xXD4Js4RWOpcvXpcsePIqlnlJ2XsTgoX6S33Cvz2yUHzrOubMHXHbz3EYPa+nahQ4N6VUVq9JcA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.4.0-canary.31':
resolution: {integrity: sha512-vylmxjjBjA+uwH4ZO1Mkv7OKH3cXAWgZp1sBRMTClBbKyMcU/WgV3DPhjP3t+gsYYqaFpTJg87JVPbha0uzlqQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.4.0-canary.11':
resolution: {integrity: sha512-0hGchpdzp6LyFBIPoE0rFLjbW1uoFGucgqAtcuw4M5+vTqTJSdh6m1kxJ9LY7IhCs+j0TOxXsuHHUgkPcma6vQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-gnu@15.4.0-canary.31':
resolution: {integrity: sha512-Yakxjs+uzfwTy5HbSa8NPKNbw8pad+p7T86p52/+7eFZab0QoPAI1v1dkscvz2D5+luQLHRZPS9Dgxljv/cgvA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.4.0-canary.11':
resolution: {integrity: sha512-rGe/4JSOR9GfZHa48mpZd3mBH9OPpIEmbkm6dx4cmoi8+xRR6ctFH0w0ft/LLvQCqGZnS4fmvGDPYQQuwMnfxg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.4.0-canary.31':
resolution: {integrity: sha512-spC49gdMMtMeAb9dd8sec14cqckiOWFgyi5uv5UvyvK8EQhTX0oDpPBmzODlesasIbx0rQRYYMs8KP9msFmTPA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.4.0-canary.11':
resolution: {integrity: sha512-UoyYOZBX+PgGCLNJFY5jVKoTHPKJZuW78nEpV429AI3vHfJvbM4SAVRGVKFkqjknB2Y8NeMyg4YUoSTlpwf3fQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-arm64-msvc@15.4.0-canary.31':
resolution: {integrity: sha512-Np5g/RJDHHxn15mg7mkAqhwaPG/H3uYbaO6RGxLjoKy2NJzivZ+a53r6BhhgvYywTgU18syUqgmLxxmz+hjysw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.4.0-canary.11':
resolution: {integrity: sha512-VmjnbvMN2MMTyx8k7aMGS1MAO7gvFpYN/Iu5U2cRpDvCximA5Fn9LwmBWYTf4aj+e4dk0zn5TL2lSa0HzGdTzQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@next/swc-win32-x64-msvc@15.4.0-canary.31':
resolution: {integrity: sha512-0/4BgXFG9N/YT+pHN1XvPB5JJTyJKrTUNvZPdCN7knnonXwuhoOwqmK/xI3rGLbHHTe0UFGcsXzh1NeRjRyjUw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@noble/ciphers@1.2.1':
resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==}
engines: {node: ^14.21.3 || >=16}
@@ -5273,16 +5303,16 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@rushstack/node-core-library@4.1.0':
resolution: {integrity: sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==}
'@rushstack/node-core-library@5.13.0':
resolution: {integrity: sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
'@rushstack/node-core-library@5.13.0':
resolution: {integrity: sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==}
'@rushstack/node-core-library@5.13.1':
resolution: {integrity: sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
@@ -5300,12 +5330,20 @@ packages:
'@types/node':
optional: true
'@rushstack/ts-command-line@4.17.1':
resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==}
'@rushstack/terminal@0.15.3':
resolution: {integrity: sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g==}
peerDependencies:
'@types/node': '*'
peerDependenciesMeta:
'@types/node':
optional: true
'@rushstack/ts-command-line@4.23.7':
resolution: {integrity: sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==}
'@rushstack/ts-command-line@5.0.1':
resolution: {integrity: sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q==}
'@sapphire/async-queue@1.5.5':
resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==}
engines: {node: '>=v14.0.0', npm: '>=7.0.0'}
@@ -6210,9 +6248,6 @@ packages:
'@types/istanbul-reports@3.0.4':
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
'@types/jest@29.5.14':
resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==}
'@types/jsdoc-to-markdown@7.0.6':
resolution: {integrity: sha512-FB/oOam8P4WoGbkfLu6ciektQhqlVuL4VsbrGJp3/YDAlRGcoiOhXDnnPL73TtHYMsDZ7NHYhCGJn4hu0TZdHg==}
@@ -7799,10 +7834,6 @@ packages:
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
colors@1.2.5:
resolution: {integrity: sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==}
engines: {node: '>=0.1.90'}
colors@1.4.0:
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
engines: {node: '>=0.1.90'}
@@ -7848,10 +7879,6 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
comment-parser@1.4.1:
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
engines: {node: '>= 12.0.0'}
@@ -9257,10 +9284,6 @@ packages:
fs-extra@6.0.1:
resolution: {integrity: sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
@@ -10606,10 +10629,6 @@ packages:
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
@@ -11212,6 +11231,27 @@ packages:
sass:
optional: true
next@15.4.0-canary.31:
resolution: {integrity: sha512-w3PUyFAfICyamYgvS2ZZqK8rSSyI97FFoVH5S9E/QABs4LLF50JyhsJwVZD7cADpMsBqBKaVes6OYltBxP8eIw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@playwright/test':
optional: true
babel-plugin-react-compiler:
optional: true
sass:
optional: true
no-case@2.3.2:
resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
@@ -12349,9 +12389,6 @@ packages:
resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==}
engines: {node: '>=10'}
resolve@1.19.0:
resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@@ -13693,10 +13730,6 @@ packages:
resolution: {integrity: sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==}
engines: {node: ^18.17.0 || >=20.5.0}
validator@13.12.0:
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
engines: {node: '>= 0.10'}
vercel@41.4.1:
resolution: {integrity: sha512-o7zsdAuFls+gMotvzoXFIe6pdsxsWmkdeQx7sAw4qbNgmDe80e0/C8hLEUs3wLO8NRI5E1AlwS2MM8vYs+ljOg==}
engines: {node: '>= 18'}
@@ -14053,11 +14086,6 @@ packages:
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
engines: {node: '>=18'}
z-schema@5.0.5:
resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==}
engines: {node: '>=8.0.0'}
hasBin: true
zlib-sync@0.1.10:
resolution: {integrity: sha512-t7/pYg5tLBznL1RuhmbAt8rNp5tbhr+TSrJFnMkRtrGIaPJZ6Dc0uR4u3OoQI2d6cGlVI62E3Gy6gwkxyIqr/w==}
@@ -15849,41 +15877,6 @@ snapshots:
jest-util: 29.7.0
slash: 3.0.0
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))':
dependencies:
'@jest/console': 29.7.0
'@jest/reporters': 29.7.0
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.15.2
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
jest-resolve: 29.7.0
jest-resolve-dependencies: 29.7.0
jest-runner: 29.7.0
jest-runtime: 29.7.0
jest-snapshot: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
jest-watcher: 29.7.0
micromatch: 4.0.8
pretty-format: 29.7.0
slash: 3.0.0
strip-ansi: 6.0.1
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
- ts-node
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3))':
dependencies:
'@jest/console': 29.7.0
@@ -16131,7 +16124,7 @@ snapshots:
'@microsoft/api-extractor-model@7.30.5(@types/node@18.17.9)':
dependencies:
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@microsoft/tsdoc-config': 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library': 5.13.0(@types/node@18.17.9)
transitivePeerDependencies:
- '@types/node'
@@ -16140,7 +16133,7 @@ snapshots:
'@microsoft/api-extractor-model@7.30.5(@types/node@22.15.2)':
dependencies:
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@microsoft/tsdoc-config': 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library': 5.13.0(@types/node@22.15.2)
transitivePeerDependencies:
- '@types/node'
@@ -16149,7 +16142,7 @@ snapshots:
dependencies:
'@microsoft/api-extractor-model': 7.30.5(@types/node@18.17.9)
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@microsoft/tsdoc-config': 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library': 5.13.0(@types/node@18.17.9)
'@rushstack/rig-package': 0.5.3
'@rushstack/terminal': 0.15.2(@types/node@18.17.9)
@@ -16168,7 +16161,7 @@ snapshots:
dependencies:
'@microsoft/api-extractor-model': 7.30.5(@types/node@22.15.2)
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@microsoft/tsdoc-config': 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
'@rushstack/node-core-library': 5.13.0(@types/node@22.15.2)
'@rushstack/rig-package': 0.5.3
'@rushstack/terminal': 0.15.2(@types/node@22.15.2)
@@ -16182,22 +16175,13 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/tsdoc-config@0.16.2(patch_hash=cda37396c30a2865185c82c3ac8d7d5a0b1c5eebab1dbca7a3c29e3c17d96247)':
dependencies:
'@microsoft/tsdoc': 0.14.2
ajv: 6.12.6
jju: 1.4.0
resolve: 1.19.0
'@microsoft/tsdoc-config@0.17.1':
'@microsoft/tsdoc-config@0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)':
dependencies:
'@microsoft/tsdoc': 0.15.1
ajv: 8.12.0
jju: 1.4.0
resolve: 1.22.10
'@microsoft/tsdoc@0.14.2': {}
'@microsoft/tsdoc@0.15.1': {}
'@msgpack/msgpack@3.1.1': {}
@@ -16217,6 +16201,8 @@ snapshots:
'@next/env@15.4.0-canary.11': {}
'@next/env@15.4.0-canary.31': {}
'@next/eslint-plugin-next@15.2.4':
dependencies:
fast-glob: 3.3.1
@@ -16224,27 +16210,51 @@ snapshots:
'@next/swc-darwin-arm64@15.4.0-canary.11':
optional: true
'@next/swc-darwin-arm64@15.4.0-canary.31':
optional: true
'@next/swc-darwin-x64@15.4.0-canary.11':
optional: true
'@next/swc-darwin-x64@15.4.0-canary.31':
optional: true
'@next/swc-linux-arm64-gnu@15.4.0-canary.11':
optional: true
'@next/swc-linux-arm64-gnu@15.4.0-canary.31':
optional: true
'@next/swc-linux-arm64-musl@15.4.0-canary.11':
optional: true
'@next/swc-linux-arm64-musl@15.4.0-canary.31':
optional: true
'@next/swc-linux-x64-gnu@15.4.0-canary.11':
optional: true
'@next/swc-linux-x64-gnu@15.4.0-canary.31':
optional: true
'@next/swc-linux-x64-musl@15.4.0-canary.11':
optional: true
'@next/swc-linux-x64-musl@15.4.0-canary.31':
optional: true
'@next/swc-win32-arm64-msvc@15.4.0-canary.11':
optional: true
'@next/swc-win32-arm64-msvc@15.4.0-canary.31':
optional: true
'@next/swc-win32-x64-msvc@15.4.0-canary.11':
optional: true
'@next/swc-win32-x64-msvc@15.4.0-canary.31':
optional: true
'@noble/ciphers@1.2.1': {}
'@nodelib/fs.scandir@2.1.5':
@@ -18324,17 +18334,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
'@rushstack/node-core-library@4.1.0(@types/node@22.15.2)':
dependencies:
fs-extra: 7.0.1
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.10
semver: 7.5.4
z-schema: 5.0.5
optionalDependencies:
'@types/node': 22.15.2
'@rushstack/node-core-library@5.13.0(@types/node@18.17.9)':
dependencies:
ajv: 8.13.0
@@ -18362,6 +18361,19 @@ snapshots:
optionalDependencies:
'@types/node': 22.15.2
'@rushstack/node-core-library@5.13.1(@types/node@22.15.2)':
dependencies:
ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0)
ajv-formats: 3.0.1
fs-extra: 11.3.0
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.10
semver: 7.5.4
optionalDependencies:
'@types/node': 22.15.2
'@rushstack/rig-package@0.5.3':
dependencies:
resolve: 1.22.10
@@ -18382,12 +18394,12 @@ snapshots:
optionalDependencies:
'@types/node': 22.15.2
'@rushstack/ts-command-line@4.17.1':
'@rushstack/terminal@0.15.3(@types/node@22.15.2)':
dependencies:
'@types/argparse': 1.0.38
argparse: 1.0.10
colors: 1.2.5
string-argv: 0.3.2
'@rushstack/node-core-library': 5.13.1(@types/node@22.15.2)
supports-color: 8.1.1
optionalDependencies:
'@types/node': 22.15.2
'@rushstack/ts-command-line@4.23.7(@types/node@18.17.9)':
dependencies:
@@ -18408,6 +18420,15 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@rushstack/ts-command-line@5.0.1(@types/node@22.15.2)':
dependencies:
'@rushstack/terminal': 0.15.3(@types/node@22.15.2)
'@types/argparse': 1.0.38
argparse: 1.0.10
string-argv: 0.3.2
transitivePeerDependencies:
- '@types/node'
'@sapphire/async-queue@1.5.5': {}
'@sapphire/fetch@3.0.5': {}
@@ -19705,11 +19726,6 @@ snapshots:
dependencies:
'@types/istanbul-lib-report': 3.0.3
'@types/jest@29.5.14':
dependencies:
expect: 29.7.0
pretty-format: 29.7.0
'@types/jsdoc-to-markdown@7.0.6': {}
'@types/json-schema@7.0.15': {}
@@ -20638,6 +20654,11 @@ snapshots:
next: 15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
'@vercel/analytics@1.5.0(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)':
optionalDependencies:
next: 15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
'@vercel/blob@0.27.3':
dependencies:
async-retry: 1.3.3
@@ -21845,8 +21866,6 @@ snapshots:
colorette@2.0.20: {}
colors@1.2.5: {}
colors@1.4.0: {}
combined-stream@1.0.8:
@@ -21889,9 +21908,6 @@ snapshots:
commander@4.1.1: {}
commander@9.5.0:
optional: true
comment-parser@1.4.1: {}
common-sequence@2.0.2: {}
@@ -22109,21 +22125,6 @@ snapshots:
p-filter: 3.0.0
p-map: 6.0.0
create-jest@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4)):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
create-jest@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3)):
dependencies:
'@jest/types': 29.6.3
@@ -23442,7 +23443,7 @@ snapshots:
eslint-plugin-tsdoc@0.4.0:
dependencies:
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@microsoft/tsdoc-config': 0.17.1(patch_hash=3b647448c34391a3eb391ebdbe252924e783e3bb796def00b999a7cf147856f8)
eslint-plugin-typescript-sort-keys@3.3.0(@typescript-eslint/parser@8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4))(eslint@9.25.1(jiti@2.4.2))(typescript@5.5.4):
dependencies:
@@ -23903,12 +23904,6 @@ snapshots:
jsonfile: 4.0.0
universalify: 0.1.2
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-minipass@2.1.0:
dependencies:
minipass: 3.3.6
@@ -24061,6 +24056,10 @@ snapshots:
dependencies:
next: 15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
geist@1.3.1(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)):
dependencies:
next: 15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
generic-pool@3.4.2: {}
gensync@1.0.0-beta.2: {}
@@ -24978,25 +24977,6 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
exit: 0.1.2
import-local: 3.2.0
jest-config: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest-cli@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3))
@@ -25016,37 +24996,6 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4)):
dependencies:
'@babel/core': 7.26.10
'@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
babel-jest: 29.7.0(@babel/core@7.26.10)
chalk: 4.1.2
ci-info: 3.9.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.11
jest-circus: 29.7.0
jest-environment-node: 29.7.0
jest-get-type: 29.6.3
jest-regex-util: 29.6.3
jest-resolve: 29.7.0
jest-runner: 29.7.0
jest-util: 29.7.0
jest-validate: 29.7.0
micromatch: 4.0.8
parse-json: 5.2.0
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.15.2
ts-node: 10.9.2(@types/node@22.15.2)(typescript@5.5.4)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
jest-config@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3)):
dependencies:
'@babel/core': 7.26.10
@@ -25293,18 +25242,6 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
'@jest/types': 29.6.3
import-local: 3.2.0
jest-cli: 29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest@29.7.0(@types/node@22.15.2)(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3))
@@ -25648,8 +25585,6 @@ snapshots:
lodash.isarguments@3.1.0: {}
lodash.isequal@4.5.0: {}
lodash.isplainobject@4.0.6: {}
lodash.kebabcase@4.1.1: {}
@@ -26506,6 +26441,30 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@next/env': 15.4.0-canary.31
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001711
postcss: 8.4.31
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
styled-jsx: 5.1.6(react@19.1.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.4.0-canary.31
'@next/swc-darwin-x64': 15.4.0-canary.31
'@next/swc-linux-arm64-gnu': 15.4.0-canary.31
'@next/swc-linux-arm64-musl': 15.4.0-canary.31
'@next/swc-linux-x64-gnu': 15.4.0-canary.31
'@next/swc-linux-x64-musl': 15.4.0-canary.31
'@next/swc-win32-arm64-msvc': 15.4.0-canary.31
'@next/swc-win32-x64-msvc': 15.4.0-canary.31
babel-plugin-react-compiler: 19.1.0-rc.1
sharp: 0.34.1
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
no-case@2.3.2:
dependencies:
lower-case: 1.1.4
@@ -26678,6 +26637,13 @@ snapshots:
optionalDependencies:
next: 15.4.0-canary.11(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
nuqs@2.4.3(next@15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
mitt: 3.0.1
react: 19.1.0
optionalDependencies:
next: 15.4.0-canary.31(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
oauth-sign@0.9.0: {}
object-assign@4.1.1: {}
@@ -27782,11 +27748,6 @@ snapshots:
resolve.exports@2.0.3: {}
resolve@1.19.0:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@@ -28710,25 +28671,6 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
ts-node@10.9.2(@types/node@22.15.2)(typescript@5.5.4):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.15.2
acorn: 8.14.1
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.5.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optional: true
ts-node@10.9.2(@types/node@22.15.2)(typescript@5.8.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
@@ -29404,8 +29346,6 @@ snapshots:
validate-npm-package-name@6.0.0: {}
validator@13.12.0: {}
vercel@41.4.1(encoding@0.1.13)(rollup@4.39.0):
dependencies:
'@vercel/build-utils': 10.5.1
@@ -29892,14 +29832,6 @@ snapshots:
yoctocolors@2.1.1: {}
z-schema@5.0.5:
dependencies:
lodash.get: 4.4.2
lodash.isequal: 4.5.0
validator: 13.12.0
optionalDependencies:
commander: 9.5.0
zlib-sync@0.1.10:
dependencies:
nan: 2.22.0

View File

@@ -29,4 +29,4 @@ publicHoistPattern:
- '*jju*'
patchedDependencies:
'@microsoft/tsdoc-config@0.16.2': patches/@microsoft__tsdoc-config@0.16.2.patch
'@microsoft/tsdoc-config@0.17.1': patches/@microsoft__tsdoc-config@0.17.1.patch