diff --git a/.github/labeler.yml b/.github/labeler.yml index 91c0c1e1f..29fcb2cd5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,6 +4,12 @@ apps:guide: apps:website: - apps/website/* - apps/website/**/* +packages:api-extractor: + - packages/api-extractor/* + - packages/api-extractor/**/* +packages:api-extractor-model: + - packages/api-extractor-model/* + - packages/api-extractor-model/**/* packages:brokers: - packages/brokers/* - packages/brokers/**/* diff --git a/.github/labels.yml b/.github/labels.yml index 9df9b3a4b..de63dc0ef 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -52,6 +52,10 @@ color: e4e669 - name: need repro color: c66037 +- name: packages:api-extractor + color: fbca04 +- name: packages:api-extractor-model + color: fbca04 - name: packages:brokers color: fbca04 - name: packages:builders diff --git a/.npmrc b/.npmrc index 3c63d3a57..344e3ff53 100644 --- a/.npmrc +++ b/.npmrc @@ -3,5 +3,4 @@ resolution-mode=highest public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* public-hoist-pattern[]=*@rushstack/node-core-library* -public-hoist-pattern[]=*@microsoft/api-extractor-model* public-hoist-pattern[]=*jju* diff --git a/api-extractor.json b/api-extractor.json index 1f3e6f79d..736568c70 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -222,7 +222,7 @@ /** * (REQUIRED) Whether to generate the .d.ts rollup file. */ - "enabled": true, + "enabled": false, /** * Specifies the output path for a .d.ts rollup file to be generated without any trimming. diff --git a/apps/website/next.config.js b/apps/website/next.config.js index f5cc17082..1b3041379 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.js @@ -8,7 +8,7 @@ export default withBundleAnalyzer({ reactStrictMode: true, experimental: { typedRoutes: true, - serverComponentsExternalPackages: ['@rushstack/node-core-library', '@microsoft/api-extractor-model', 'jju'], + serverComponentsExternalPackages: ['@rushstack/node-core-library', '@discordjs/api-extractor-model', 'jju'], }, images: { dangerouslyAllowSVG: true, diff --git a/apps/website/package.json b/apps/website/package.json index d7a1b412c..74e083246 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -50,7 +50,7 @@ "@discordjs/api-extractor-utils": "workspace:^", "@discordjs/scripts": "workspace:^", "@discordjs/ui": "workspace:^", - "@microsoft/api-extractor-model": "^7.28.2", + "@discordjs/api-extractor-model": "workspace:^", "@microsoft/tsdoc": "^0.14.2", "@microsoft/tsdoc-config": "0.16.2", "@planetscale/database": "^1.11.0", diff --git a/apps/website/src/app/api/dynamic-open-graph.png/route.tsx b/apps/website/src/app/api/dynamic-open-graph.png/route.tsx index 3e29d5da6..2f3d6c539 100644 --- a/apps/website/src/app/api/dynamic-open-graph.png/route.tsx +++ b/apps/website/src/app/api/dynamic-open-graph.png/route.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-unknown-property */ -import type { ApiItemKind } from '@microsoft/api-extractor-model'; +import type { ApiItemKind } from '@discordjs/api-extractor-model'; import { ImageResponse } from '@vercel/og'; import type { NextRequest } from 'next/server'; diff --git a/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx b/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx index 2255fae0a..fbb30929a 100644 --- a/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx +++ b/apps/website/src/app/docs/packages/[package]/[version]/[item]/page.tsx @@ -1,4 +1,3 @@ -import { tryResolveSummaryText } from '@discordjs/scripts'; import type { ApiClass, ApiDeclaredItem, @@ -13,8 +12,9 @@ import type { ApiTypeAlias, ApiVariable, ApiFunction, -} from '@microsoft/api-extractor-model'; -import { ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; +import { ApiItemKind, ApiModel } from '@discordjs/api-extractor-model'; +import { tryResolveSummaryText } from '@discordjs/scripts'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { fetchModelJSON } from '~/app/docAPI'; diff --git a/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx b/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx index 913f73389..cfa8a13ab 100644 --- a/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx +++ b/apps/website/src/app/docs/packages/[package]/[version]/layout.tsx @@ -1,5 +1,5 @@ -import type { ApiFunction, ApiItem } from '@microsoft/api-extractor-model'; -import { ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiFunction, ApiItem } from '@discordjs/api-extractor-model'; +import { ApiModel } from '@discordjs/api-extractor-model'; import dynamic from 'next/dynamic'; import { notFound } from 'next/navigation'; import type { PropsWithChildren } from 'react'; diff --git a/apps/website/src/components/Badges.tsx b/apps/website/src/components/Badges.tsx index 68349269e..206e0ffde 100644 --- a/apps/website/src/components/Badges.tsx +++ b/apps/website/src/components/Badges.tsx @@ -1,5 +1,5 @@ -import type { ApiDocumentedItem } from '@microsoft/api-extractor-model'; -import { ApiAbstractMixin, ApiProtectedMixin, ApiReadonlyMixin, ApiStaticMixin } from '@microsoft/api-extractor-model'; +import type { ApiDocumentedItem } from '@discordjs/api-extractor-model'; +import { ApiAbstractMixin, ApiProtectedMixin, ApiReadonlyMixin, ApiStaticMixin } from '@discordjs/api-extractor-model'; import type { PropsWithChildren } from 'react'; export enum BadgeColor { diff --git a/apps/website/src/components/CmdK.tsx b/apps/website/src/components/CmdK.tsx index 6e39952fe..c50442fd9 100644 --- a/apps/website/src/components/CmdK.tsx +++ b/apps/website/src/components/CmdK.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { ApiItemKind } from '@microsoft/api-extractor-model'; +import type { ApiItemKind } from '@discordjs/api-extractor-model'; import { VscArrowRight } from '@react-icons/all-files/vsc/VscArrowRight'; import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass'; import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum'; diff --git a/apps/website/src/components/CodeHeading.tsx b/apps/website/src/components/CodeHeading.tsx index 27f0c7dc8..655de27f3 100644 --- a/apps/website/src/components/CodeHeading.tsx +++ b/apps/website/src/components/CodeHeading.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; import { Anchor } from './Anchor'; +import { SourceLink } from './documentation/SourceLink'; export interface CodeListingProps { /** @@ -14,15 +15,26 @@ export interface CodeListingProps { * The href of this heading. */ readonly href?: string | undefined; + /** + * The line in the source code where this part is declared + */ + readonly sourceLine?: number | undefined; + /** + * The URL of the source code of this code part + */ + readonly sourceURL?: string | undefined; } -export function CodeHeading({ href, className, children }: CodeListingProps) { +export function CodeHeading({ href, className, children, sourceURL, sourceLine }: CodeListingProps) { return ( -
- {href ? : null} - {children} +
+
+ {href ? : null} + {children} +
+ {sourceURL ? : null}
); } diff --git a/apps/website/src/components/ExcerptText.tsx b/apps/website/src/components/ExcerptText.tsx index ccf6de4b0..532035ddc 100644 --- a/apps/website/src/components/ExcerptText.tsx +++ b/apps/website/src/components/ExcerptText.tsx @@ -1,5 +1,5 @@ -import type { ApiModel, Excerpt } from '@microsoft/api-extractor-model'; -import { ExcerptTokenKind } from '@microsoft/api-extractor-model'; +import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model'; +import { ExcerptTokenKind } from '@discordjs/api-extractor-model'; import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants'; import { ItemLink } from './ItemLink'; import { resolveItemURI } from './documentation/util'; @@ -60,7 +60,7 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) { ); } - return token.text; + return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, ''); })} ); diff --git a/apps/website/src/components/InheritanceText.tsx b/apps/website/src/components/InheritanceText.tsx index 8d99c98d6..4f97a426c 100644 --- a/apps/website/src/components/InheritanceText.tsx +++ b/apps/website/src/components/InheritanceText.tsx @@ -1,4 +1,4 @@ -import type { ApiDeclaredItem } from '@microsoft/api-extractor-model'; +import type { ApiDeclaredItem } from '@discordjs/api-extractor-model'; import { ItemLink } from './ItemLink'; import { resolveItemURI } from './documentation/util'; diff --git a/apps/website/src/components/ParameterTable.tsx b/apps/website/src/components/ParameterTable.tsx index dc6534cc0..2ad0b8426 100644 --- a/apps/website/src/components/ParameterTable.tsx +++ b/apps/website/src/components/ParameterTable.tsx @@ -1,4 +1,4 @@ -import type { ApiDocumentedItem, ApiParameterListMixin } from '@microsoft/api-extractor-model'; +import type { ApiDocumentedItem, ApiParameterListMixin } from '@discordjs/api-extractor-model'; import { useMemo } from 'react'; import { resolveParameters } from '~/util/model'; import { ExcerptText } from './ExcerptText'; @@ -16,7 +16,7 @@ export function ParameterTable({ item }: { readonly item: ApiDocumentedItem & Ap const rows = useMemo( () => params.map((param) => ({ - Name: param.name, + Name: param.isRest ? `...${param.name}` : param.name, Type: , Optional: param.isOptional ? 'Yes' : 'No', Description: param.description ? : 'None', diff --git a/apps/website/src/components/Property.tsx b/apps/website/src/components/Property.tsx index be5f5874b..75739f3de 100644 --- a/apps/website/src/components/Property.tsx +++ b/apps/website/src/components/Property.tsx @@ -3,7 +3,7 @@ import type { ApiItemContainerMixin, ApiProperty, ApiPropertySignature, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import type { PropsWithChildren } from 'react'; import { Badges } from './Badges'; import { CodeHeading } from './CodeHeading'; @@ -25,7 +25,11 @@ export function Property({
- + {`${item.displayName}${item.isOptional ? '?' : ''}`} : {item.propertyTypeExcerpt.text ? ( diff --git a/apps/website/src/components/PropertyList.tsx b/apps/website/src/components/PropertyList.tsx index 8b98f5c22..8e922513a 100644 --- a/apps/website/src/components/PropertyList.tsx +++ b/apps/website/src/components/PropertyList.tsx @@ -4,8 +4,8 @@ import type { ApiItemContainerMixin, ApiProperty, ApiPropertySignature, -} from '@microsoft/api-extractor-model'; -import { ApiItemKind } from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; import { Fragment, useMemo } from 'react'; import { resolveMembers } from '~/util/members'; import { Property } from './Property'; diff --git a/apps/website/src/components/Sidebar.tsx b/apps/website/src/components/Sidebar.tsx index b75588b3a..8167294c2 100644 --- a/apps/website/src/components/Sidebar.tsx +++ b/apps/website/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { ApiItemKind } from '@microsoft/api-extractor-model'; +import type { ApiItemKind } from '@discordjs/api-extractor-model'; import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass'; import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum'; import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface'; diff --git a/apps/website/src/components/SignatureText.tsx b/apps/website/src/components/SignatureText.tsx index e3ff6c033..58168c184 100644 --- a/apps/website/src/components/SignatureText.tsx +++ b/apps/website/src/components/SignatureText.tsx @@ -1,4 +1,4 @@ -import type { ApiModel, Excerpt } from '@microsoft/api-extractor-model'; +import type { ApiModel, Excerpt } from '@discordjs/api-extractor-model'; import { ExcerptText } from './ExcerptText'; export function SignatureText({ excerpt, model }: { readonly excerpt: Excerpt; readonly model: ApiModel }) { diff --git a/apps/website/src/components/TypeParamTable.tsx b/apps/website/src/components/TypeParamTable.tsx index 8ce2ed9c8..e0601638f 100644 --- a/apps/website/src/components/TypeParamTable.tsx +++ b/apps/website/src/components/TypeParamTable.tsx @@ -1,4 +1,4 @@ -import type { ApiTypeParameterListMixin } from '@microsoft/api-extractor-model'; +import type { ApiTypeParameterListMixin } from '@discordjs/api-extractor-model'; import { useMemo } from 'react'; import { ExcerptText } from './ExcerptText'; import { Table } from './Table'; diff --git a/apps/website/src/components/documentation/Header.tsx b/apps/website/src/components/documentation/Header.tsx index efbd3f2e2..96dc2f4cc 100644 --- a/apps/website/src/components/documentation/Header.tsx +++ b/apps/website/src/components/documentation/Header.tsx @@ -1,11 +1,11 @@ -import { ApiItemKind } from '@microsoft/api-extractor-model'; -import { VscFileCode } from '@react-icons/all-files/vsc/VscFileCode'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass'; import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum'; import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable'; import type { PropsWithChildren } from 'react'; +import { SourceLink } from './SourceLink'; function generateIcon(kind: ApiItemKind) { switch (kind) { @@ -30,7 +30,13 @@ export function Header({ kind, name, sourceURL, -}: PropsWithChildren<{ readonly kind: ApiItemKind; readonly name: string; readonly sourceURL?: string | undefined }>) { + sourceLine, +}: PropsWithChildren<{ + readonly kind: ApiItemKind; + readonly name: string; + readonly sourceLine?: number | undefined; + readonly sourceURL?: string | undefined; +}>) { return (

@@ -38,11 +44,7 @@ export function Header({ {generateIcon(kind)} {name} - {sourceURL ? ( - - - - ) : null} + {sourceURL ? : null}

); diff --git a/apps/website/src/components/documentation/HierarchyText.tsx b/apps/website/src/components/documentation/HierarchyText.tsx index 346b12f12..b68bf4b39 100644 --- a/apps/website/src/components/documentation/HierarchyText.tsx +++ b/apps/website/src/components/documentation/HierarchyText.tsx @@ -1,5 +1,5 @@ -import type { ApiClass, ApiInterface, Excerpt } from '@microsoft/api-extractor-model'; -import { ApiItemKind } from '@microsoft/api-extractor-model'; +import type { ApiClass, ApiInterface, Excerpt } from '@discordjs/api-extractor-model'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; import { ExcerptText } from '../ExcerptText'; export function HierarchyText({ diff --git a/apps/website/src/components/documentation/Members.tsx b/apps/website/src/components/documentation/Members.tsx index 1583af270..42f9ef01a 100644 --- a/apps/website/src/components/documentation/Members.tsx +++ b/apps/website/src/components/documentation/Members.tsx @@ -1,4 +1,4 @@ -import type { ApiDeclaredItem, ApiItemContainerMixin } from '@microsoft/api-extractor-model'; +import type { ApiDeclaredItem, ApiItemContainerMixin } from '@discordjs/api-extractor-model'; import { MethodsSection } from './section/MethodsSection'; import { PropertiesSection } from './section/PropertiesSection'; import { hasProperties, hasMethods } from './util'; diff --git a/apps/website/src/components/documentation/ObjectHeader.tsx b/apps/website/src/components/documentation/ObjectHeader.tsx index fba3b8281..198e54ac2 100644 --- a/apps/website/src/components/documentation/ObjectHeader.tsx +++ b/apps/website/src/components/documentation/ObjectHeader.tsx @@ -1,4 +1,4 @@ -import type { ApiDeclaredItem } from '@microsoft/api-extractor-model'; +import type { ApiDeclaredItem } from '@discordjs/api-extractor-model'; import { SyntaxHighlighter } from '../SyntaxHighlighter'; import { Header } from './Header'; import { SummarySection } from './section/SummarySection'; @@ -10,7 +10,12 @@ export interface ObjectHeaderProps { export function ObjectHeader({ item }: ObjectHeaderProps) { return ( <> -
+
{/* @ts-expect-error async component */} diff --git a/apps/website/src/components/documentation/SourceLink.tsx b/apps/website/src/components/documentation/SourceLink.tsx new file mode 100644 index 000000000..9ecc9115e --- /dev/null +++ b/apps/website/src/components/documentation/SourceLink.tsx @@ -0,0 +1,22 @@ +import { VscFileCode } from '@react-icons/all-files/vsc/VscFileCode'; + +export function SourceLink({ + className, + sourceURL, + sourceLine, +}: { + readonly className?: string | undefined; + readonly sourceLine?: number | undefined; + readonly sourceURL?: string | undefined; +}) { + return ( + + + + ); +} diff --git a/apps/website/src/components/documentation/section/ConstructorSection.tsx b/apps/website/src/components/documentation/section/ConstructorSection.tsx index 232a4289e..c06d9e7ca 100644 --- a/apps/website/src/components/documentation/section/ConstructorSection.tsx +++ b/apps/website/src/components/documentation/section/ConstructorSection.tsx @@ -1,4 +1,4 @@ -import type { ApiConstructor } from '@microsoft/api-extractor-model'; +import type { ApiConstructor } from '@discordjs/api-extractor-model'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { CodeHeading } from '~/components/CodeHeading'; import { ParameterTable } from '../../ParameterTable'; @@ -10,7 +10,10 @@ export function ConstructorSection({ item }: { readonly item: ApiConstructor }) return ( } padded title="Constructor">
- {`constructor(${parametersString(item)})`} + {`constructor(${parametersString(item)})`} {item.tsdocComment ? : null}
diff --git a/apps/website/src/components/documentation/section/MethodsSection.tsx b/apps/website/src/components/documentation/section/MethodsSection.tsx index 3dd5d90b6..f68479d45 100644 --- a/apps/website/src/components/documentation/section/MethodsSection.tsx +++ b/apps/website/src/components/documentation/section/MethodsSection.tsx @@ -4,8 +4,8 @@ import type { ApiItemContainerMixin, ApiMethod, ApiMethodSignature, -} from '@microsoft/api-extractor-model'; -import { ApiItemKind } from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { useMemo, Fragment } from 'react'; import { resolveMembers } from '~/util/members'; diff --git a/apps/website/src/components/documentation/section/ParametersSection.tsx b/apps/website/src/components/documentation/section/ParametersSection.tsx index 0b0eb18c3..71f0d3832 100644 --- a/apps/website/src/components/documentation/section/ParametersSection.tsx +++ b/apps/website/src/components/documentation/section/ParametersSection.tsx @@ -1,4 +1,4 @@ -import type { ApiDocumentedItem, ApiParameterListMixin } from '@microsoft/api-extractor-model'; +import type { ApiDocumentedItem, ApiParameterListMixin } from '@discordjs/api-extractor-model'; import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter'; import { ParameterTable } from '../../ParameterTable'; import { DocumentationSection } from './DocumentationSection'; diff --git a/apps/website/src/components/documentation/section/PropertiesSection.tsx b/apps/website/src/components/documentation/section/PropertiesSection.tsx index 6ae6a34fb..999201b97 100644 --- a/apps/website/src/components/documentation/section/PropertiesSection.tsx +++ b/apps/website/src/components/documentation/section/PropertiesSection.tsx @@ -1,4 +1,4 @@ -import type { ApiItemContainerMixin } from '@microsoft/api-extractor-model'; +import type { ApiItemContainerMixin } from '@discordjs/api-extractor-model'; import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty'; import { PropertyList } from '../../PropertyList'; import { DocumentationSection } from './DocumentationSection'; diff --git a/apps/website/src/components/documentation/section/SummarySection.tsx b/apps/website/src/components/documentation/section/SummarySection.tsx index 8bdd49ede..c47287b6a 100644 --- a/apps/website/src/components/documentation/section/SummarySection.tsx +++ b/apps/website/src/components/documentation/section/SummarySection.tsx @@ -1,4 +1,4 @@ -import type { ApiDeclaredItem } from '@microsoft/api-extractor-model'; +import type { ApiDeclaredItem } from '@discordjs/api-extractor-model'; import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection'; import { TSDoc } from '../tsdoc/TSDoc'; import { DocumentationSection } from './DocumentationSection'; diff --git a/apps/website/src/components/documentation/section/TypeParametersSection.tsx b/apps/website/src/components/documentation/section/TypeParametersSection.tsx index 1471395c5..b93246b23 100644 --- a/apps/website/src/components/documentation/section/TypeParametersSection.tsx +++ b/apps/website/src/components/documentation/section/TypeParametersSection.tsx @@ -1,4 +1,4 @@ -import type { ApiTypeParameterListMixin } from '@microsoft/api-extractor-model'; +import type { ApiTypeParameterListMixin } from '@discordjs/api-extractor-model'; import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter'; import { TypeParamTable } from '../../TypeParamTable'; import { DocumentationSection } from './DocumentationSection'; diff --git a/apps/website/src/components/documentation/tsdoc/TSDoc.tsx b/apps/website/src/components/documentation/tsdoc/TSDoc.tsx index 492915a52..37d4eb441 100644 --- a/apps/website/src/components/documentation/tsdoc/TSDoc.tsx +++ b/apps/website/src/components/documentation/tsdoc/TSDoc.tsx @@ -1,4 +1,4 @@ -import type { ApiItem } from '@microsoft/api-extractor-model'; +import type { ApiItem } from '@discordjs/api-extractor-model'; import type { DocComment, DocFencedCode, DocLinkTag, DocNode, DocNodeContainer, DocPlainText } from '@microsoft/tsdoc'; import { DocNodeKind, StandardTags } from '@microsoft/tsdoc'; import type { Route } from 'next'; diff --git a/apps/website/src/components/documentation/util.ts b/apps/website/src/components/documentation/util.ts index dea6c6cb8..aca3c5260 100644 --- a/apps/website/src/components/documentation/util.ts +++ b/apps/website/src/components/documentation/util.ts @@ -1,4 +1,4 @@ -import { ApiItemKind } from '@microsoft/api-extractor-model'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; import type { ApiItem, ApiItemContainerMixin, @@ -8,7 +8,7 @@ import type { ApiPropertySignature, ApiDocumentedItem, ApiParameterListMixin, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import { METHOD_SEPARATOR, OVERLOAD_SEPARATOR } from '~/util/constants'; import { resolveMembers } from '~/util/members'; import { resolveParameters } from '~/util/model'; @@ -63,9 +63,9 @@ export function serializeMembers(clazz: ApiItemContainerMixin): TableOfContentsS export function parametersString(item: ApiDocumentedItem & ApiParameterListMixin) { return resolveParameters(item).reduce((prev, cur, index) => { if (index === 0) { - return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`; + return `${prev}${cur.isRest ? '...' : ''}${cur.isOptional ? `${cur.name}?` : cur.name}`; } - return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`; + return `${prev}, ${cur.isRest ? '...' : ''}${cur.isOptional ? `${cur.name}?` : cur.name}`; }, ''); } diff --git a/apps/website/src/components/model/Class.tsx b/apps/website/src/components/model/Class.tsx index 22d219b69..30c2a028b 100644 --- a/apps/website/src/components/model/Class.tsx +++ b/apps/website/src/components/model/Class.tsx @@ -1,5 +1,5 @@ -import type { ApiClass, ApiConstructor } from '@microsoft/api-extractor-model'; -import { ApiItemKind } from '@microsoft/api-extractor-model'; +import type { ApiClass, ApiConstructor } from '@discordjs/api-extractor-model'; +import { ApiItemKind } from '@discordjs/api-extractor-model'; // import { Outline } from '../Outline'; import { Badges } from '../Badges'; import { Documentation } from '../documentation/Documentation'; diff --git a/apps/website/src/components/model/Interface.tsx b/apps/website/src/components/model/Interface.tsx index 6619b3573..739138a6c 100644 --- a/apps/website/src/components/model/Interface.tsx +++ b/apps/website/src/components/model/Interface.tsx @@ -1,4 +1,4 @@ -import type { ApiInterface } from '@microsoft/api-extractor-model'; +import type { ApiInterface } from '@discordjs/api-extractor-model'; // import { Outline } from '../Outline'; import { Documentation } from '../documentation/Documentation'; import { HierarchyText } from '../documentation/HierarchyText'; diff --git a/apps/website/src/components/model/TypeAlias.tsx b/apps/website/src/components/model/TypeAlias.tsx index 114e98613..c5bb207a5 100644 --- a/apps/website/src/components/model/TypeAlias.tsx +++ b/apps/website/src/components/model/TypeAlias.tsx @@ -1,4 +1,4 @@ -import type { ApiTypeAlias } from '@microsoft/api-extractor-model'; +import type { ApiTypeAlias } from '@discordjs/api-extractor-model'; import { SyntaxHighlighter } from '../SyntaxHighlighter'; import { Documentation } from '../documentation/Documentation'; import { Header } from '../documentation/Header'; @@ -7,7 +7,12 @@ import { SummarySection } from '../documentation/section/SummarySection'; export function TypeAlias({ item }: { readonly item: ApiTypeAlias }) { return ( -
+
{/* @ts-expect-error async component */} diff --git a/apps/website/src/components/model/Variable.tsx b/apps/website/src/components/model/Variable.tsx index 8ac951e11..7a260ad74 100644 --- a/apps/website/src/components/model/Variable.tsx +++ b/apps/website/src/components/model/Variable.tsx @@ -1,4 +1,4 @@ -import type { ApiVariable } from '@microsoft/api-extractor-model'; +import type { ApiVariable } from '@discordjs/api-extractor-model'; import { Documentation } from '../documentation/Documentation'; import { ObjectHeader } from '../documentation/ObjectHeader'; diff --git a/apps/website/src/components/model/enum/Enum.tsx b/apps/website/src/components/model/enum/Enum.tsx index 0728fe529..32240fa7f 100644 --- a/apps/website/src/components/model/enum/Enum.tsx +++ b/apps/website/src/components/model/enum/Enum.tsx @@ -1,4 +1,4 @@ -import type { ApiEnum } from '@microsoft/api-extractor-model'; +import type { ApiEnum } from '@discordjs/api-extractor-model'; import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum'; import { Panel } from '../../Panel'; import { Documentation } from '../../documentation/Documentation'; diff --git a/apps/website/src/components/model/enum/EnumMember.tsx b/apps/website/src/components/model/enum/EnumMember.tsx index 727c6deef..18d95991e 100644 --- a/apps/website/src/components/model/enum/EnumMember.tsx +++ b/apps/website/src/components/model/enum/EnumMember.tsx @@ -1,4 +1,4 @@ -import type { ApiEnumMember } from '@microsoft/api-extractor-model'; +import type { ApiEnumMember } from '@discordjs/api-extractor-model'; import { CodeHeading } from '~/components/CodeHeading'; import { SignatureText } from '../../SignatureText'; import { TSDoc } from '../../documentation/tsdoc/TSDoc'; @@ -6,7 +6,12 @@ import { TSDoc } from '../../documentation/tsdoc/TSDoc'; export function EnumMember({ member }: { readonly member: ApiEnumMember }) { return (
- + {member.name} = {member.initializerExcerpt ? ( diff --git a/apps/website/src/components/model/function/Function.tsx b/apps/website/src/components/model/function/Function.tsx index 059d9c68d..4f15b27a0 100644 --- a/apps/website/src/components/model/function/Function.tsx +++ b/apps/website/src/components/model/function/Function.tsx @@ -1,4 +1,4 @@ -import type { ApiFunction } from '@microsoft/api-extractor-model'; +import type { ApiFunction } from '@discordjs/api-extractor-model'; import dynamic from 'next/dynamic'; import { Header } from '../../documentation/Header'; import { FunctionBody } from './FunctionBody'; @@ -6,7 +6,14 @@ import { FunctionBody } from './FunctionBody'; const OverloadSwitcher = dynamic(async () => import('../../OverloadSwitcher')); export function Function({ item }: { readonly item: ApiFunction }) { - const header =
; + const header = ( +
+ ); if (item.getMergedSiblings().length > 1) { const overloads = item diff --git a/apps/website/src/components/model/function/FunctionBody.tsx b/apps/website/src/components/model/function/FunctionBody.tsx index 0a4e5052e..b66165b0a 100644 --- a/apps/website/src/components/model/function/FunctionBody.tsx +++ b/apps/website/src/components/model/function/FunctionBody.tsx @@ -1,4 +1,4 @@ -import type { ApiFunction } from '@microsoft/api-extractor-model'; +import type { ApiFunction } from '@discordjs/api-extractor-model'; import { SyntaxHighlighter } from '../../SyntaxHighlighter'; import { Documentation } from '../../documentation/Documentation'; import { ParameterSection } from '../../documentation/section/ParametersSection'; diff --git a/apps/website/src/components/model/method/Method.tsx b/apps/website/src/components/model/method/Method.tsx index 30ee075eb..d18fc6206 100644 --- a/apps/website/src/components/model/method/Method.tsx +++ b/apps/website/src/components/model/method/Method.tsx @@ -3,7 +3,7 @@ import type { ApiItemContainerMixin, ApiMethod, ApiMethodSignature, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import dynamic from 'next/dynamic'; import { Fragment } from 'react'; import { MethodDocumentation } from './MethodDocumentation'; diff --git a/apps/website/src/components/model/method/MethodDocumentation.tsx b/apps/website/src/components/model/method/MethodDocumentation.tsx index 9689c84ba..889f0c8d3 100644 --- a/apps/website/src/components/model/method/MethodDocumentation.tsx +++ b/apps/website/src/components/model/method/MethodDocumentation.tsx @@ -3,7 +3,7 @@ import type { ApiItemContainerMixin, ApiMethod, ApiMethodSignature, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import { InheritanceText } from '../../InheritanceText'; import { ParameterTable } from '../../ParameterTable'; import { TSDoc } from '../../documentation/tsdoc/TSDoc'; diff --git a/apps/website/src/components/model/method/MethodHeader.tsx b/apps/website/src/components/model/method/MethodHeader.tsx index 136747acf..8b03984d2 100644 --- a/apps/website/src/components/model/method/MethodHeader.tsx +++ b/apps/website/src/components/model/method/MethodHeader.tsx @@ -1,4 +1,4 @@ -import type { ApiMethod, ApiMethodSignature } from '@microsoft/api-extractor-model'; +import type { ApiMethod, ApiMethodSignature } from '@discordjs/api-extractor-model'; import { useMemo } from 'react'; import { Badges } from '~/components/Badges'; import { CodeHeading } from '~/components/CodeHeading'; @@ -15,7 +15,11 @@ export function MethodHeader({ method }: { readonly method: ApiMethod | ApiMetho
- + {`${method.name}(${parametersString(method)})`} : diff --git a/apps/website/src/contexts/member.tsx b/apps/website/src/contexts/member.tsx index 0c0db2923..bf8a1b926 100644 --- a/apps/website/src/contexts/member.tsx +++ b/apps/website/src/contexts/member.tsx @@ -1,7 +1,7 @@ 'use client'; +import type { ApiItem } from '@discordjs/api-extractor-model'; import type { ApiItemJSON } from '@discordjs/api-extractor-utils'; -import type { ApiItem } from '@microsoft/api-extractor-model'; import { createContext, useContext, useMemo, useState } from 'react'; import type { PropsWithChildren, Dispatch, SetStateAction } from 'react'; diff --git a/apps/website/src/util/addPackageToModel.ts b/apps/website/src/util/addPackageToModel.ts index 1774ffad6..6dd4f069c 100644 --- a/apps/website/src/util/addPackageToModel.ts +++ b/apps/website/src/util/addPackageToModel.ts @@ -1,5 +1,5 @@ -import type { ApiModel, ApiPackage } from '@microsoft/api-extractor-model'; -import { ApiItem } from '@microsoft/api-extractor-model'; +import type { ApiModel, ApiPackage } from '@discordjs/api-extractor-model'; +import { ApiItem } from '@discordjs/api-extractor-model'; import { TSDocConfiguration } from '@microsoft/tsdoc'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; diff --git a/apps/website/src/util/fetchMember.ts b/apps/website/src/util/fetchMember.ts index 68385b7c9..535aa49fb 100644 --- a/apps/website/src/util/fetchMember.ts +++ b/apps/website/src/util/fetchMember.ts @@ -1,4 +1,4 @@ -import { ApiModel, ApiFunction } from '@microsoft/api-extractor-model'; +import { ApiModel, ApiFunction } from '@discordjs/api-extractor-model'; import { notFound } from 'next/navigation'; import { fetchModelJSON } from '~/app/docAPI'; import { addPackageToModel } from './addPackageToModel'; diff --git a/apps/website/src/util/members.ts b/apps/website/src/util/members.ts index be6dc5f3c..020c7363f 100644 --- a/apps/website/src/util/members.ts +++ b/apps/website/src/util/members.ts @@ -1,4 +1,4 @@ -import type { ApiItem, ApiItemContainerMixin } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiItemContainerMixin } from '@discordjs/api-extractor-model'; /** * Resolves all inherited members (including merged members) of a given parent. diff --git a/apps/website/src/util/model.ts b/apps/website/src/util/model.ts index abf82196c..75d10f482 100644 --- a/apps/website/src/util/model.ts +++ b/apps/website/src/util/model.ts @@ -4,7 +4,7 @@ import type { ApiModel, ApiParameterListMixin, Excerpt, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import type { DocSection } from '@microsoft/tsdoc'; export function findMemberByKey(model: ApiModel, packageName: string, containerKey: string) { @@ -24,6 +24,7 @@ export function findMember(model: ApiModel, packageName: string, memberName: str interface ResolvedParameter { description?: DocSection | undefined; isOptional: boolean; + isRest: boolean; name: string; parameterTypeExcerpt: Excerpt; } @@ -45,6 +46,7 @@ export function resolveParameters(item: ApiDocumentedItem & ApiParameterListMixi name: param.tsdocParamBlock?.parameterName ?? tsdocAnalog?.parameterName ?? param.name, description: param.tsdocParamBlock?.content ?? tsdocAnalog?.content, isOptional: param.isOptional, + isRest: param.isRest, parameterTypeExcerpt: param.parameterTypeExcerpt, }; }); diff --git a/eslint.config.js b/eslint.config.js index 1b528afa0..26c8a9756 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -93,6 +93,24 @@ export default [ files: [`packages/voice/**/*${commonFiles}`], rules: { 'no-restricted-globals': 0 }, }, + { + files: [`packages/api-extractor-model/**/*${commonFiles}`], + rules: { + '@typescript-eslint/no-namespace': 0, + 'no-prototype-builtins': 0, + 'consistent-this': 0, + 'unicorn/no-this-assignment': 0, + '@typescript-eslint/no-this-alias': 0, + }, + }, + { + files: [`packages/api-extractor/**/*${commonFiles}`], + rules: { + 'consistent-this': 0, + 'unicorn/no-this-assignment': 0, + '@typescript-eslint/no-this-alias': 0, + }, + }, reactRuleset, nextRuleset, edgeRuleset, diff --git a/packages/api-extractor-model/.gitignore b/packages/api-extractor-model/.gitignore new file mode 100644 index 000000000..8150f3511 --- /dev/null +++ b/packages/api-extractor-model/.gitignore @@ -0,0 +1,23 @@ +# Packages +node_modules + +# Log files +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Env +.env + +# Dist +dist + +# Miscellaneous +.turbo +.tmp +coverage diff --git a/packages/api-extractor-model/.lintstagedrc.js b/packages/api-extractor-model/.lintstagedrc.js new file mode 100644 index 000000000..dc17706a5 --- /dev/null +++ b/packages/api-extractor-model/.lintstagedrc.js @@ -0,0 +1 @@ +module.exports = require('../../.lintstagedrc.json'); diff --git a/packages/api-extractor-model/.npmignore b/packages/api-extractor-model/.npmignore new file mode 100644 index 000000000..302dbc5b0 --- /dev/null +++ b/packages/api-extractor-model/.npmignore @@ -0,0 +1,30 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README (and its variants) +# CHANGELOG (and its variants) +# LICENSE / LICENCE + +#-------------------------------------------- +# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE +#-------------------------------------------- + +# (Add your project-specific overrides here) \ No newline at end of file diff --git a/packages/api-extractor-model/.prettierignore b/packages/api-extractor-model/.prettierignore new file mode 100644 index 000000000..ee038b792 --- /dev/null +++ b/packages/api-extractor-model/.prettierignore @@ -0,0 +1,5 @@ +.turbo +coverage +dist +CHANGELOG.md +tsup.config.bundled* diff --git a/packages/api-extractor-model/.prettierrc.js b/packages/api-extractor-model/.prettierrc.js new file mode 100644 index 000000000..f004026c7 --- /dev/null +++ b/packages/api-extractor-model/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../.prettierrc.json'); diff --git a/packages/api-extractor-model/CHANGELOG.json b/packages/api-extractor-model/CHANGELOG.json new file mode 100644 index 000000000..eba624be5 --- /dev/null +++ b/packages/api-extractor-model/CHANGELOG.json @@ -0,0 +1,2297 @@ +{ + "name": "@microsoft/api-extractor-model", + "entries": [ + { + "version": "7.28.2", + "tag": "@microsoft/api-extractor-model_v7.28.2", + "date": "Thu, 28 Sep 2023 20:53:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.61.0`" + } + ] + } + }, + { + "version": "7.28.1", + "tag": "@microsoft/api-extractor-model_v7.28.1", + "date": "Tue, 26 Sep 2023 09:30:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update type-only imports to include the type modifier." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.1`" + } + ] + } + }, + { + "version": "7.28.0", + "tag": "@microsoft/api-extractor-model_v7.28.0", + "date": "Fri, 15 Sep 2023 00:36:58 GMT", + "comments": { + "minor": [ + { + "comment": "Update @types/node from 14 to 18" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.4`" + } + ] + } + }, + { + "version": "7.27.6", + "tag": "@microsoft/api-extractor-model_v7.27.6", + "date": "Tue, 08 Aug 2023 07:10:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.3`" + } + ] + } + }, + { + "version": "7.27.5", + "tag": "@microsoft/api-extractor-model_v7.27.5", + "date": "Wed, 19 Jul 2023 00:20:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.6`" + } + ] + } + }, + { + "version": "7.27.4", + "tag": "@microsoft/api-extractor-model_v7.27.4", + "date": "Thu, 06 Jul 2023 00:16:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.5`" + } + ] + } + }, + { + "version": "7.27.3", + "tag": "@microsoft/api-extractor-model_v7.27.3", + "date": "Thu, 15 Jun 2023 00:21:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.2`" + } + ] + } + }, + { + "version": "7.27.2", + "tag": "@microsoft/api-extractor-model_v7.27.2", + "date": "Wed, 07 Jun 2023 22:45:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.1`" + } + ] + } + }, + { + "version": "7.27.1", + "tag": "@microsoft/api-extractor-model_v7.27.1", + "date": "Mon, 29 May 2023 15:21:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.2`" + } + ] + } + }, + { + "version": "7.27.0", + "tag": "@microsoft/api-extractor-model_v7.27.0", + "date": "Mon, 22 May 2023 06:34:32 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the TypeScript dependency to ~5.0.4" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.0`" + } + ] + } + }, + { + "version": "7.26.9", + "tag": "@microsoft/api-extractor-model_v7.26.9", + "date": "Fri, 12 May 2023 00:23:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.0`" + } + ] + } + }, + { + "version": "7.26.8", + "tag": "@microsoft/api-extractor-model_v7.26.8", + "date": "Thu, 04 May 2023 00:20:28 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a mistake in the documentation for ApiParameterListMixin.overloadIndex" + } + ] + } + }, + { + "version": "7.26.7", + "tag": "@microsoft/api-extractor-model_v7.26.7", + "date": "Mon, 01 May 2023 15:23:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.58.0`" + } + ] + } + }, + { + "version": "7.26.6", + "tag": "@microsoft/api-extractor-model_v7.26.6", + "date": "Sat, 29 Apr 2023 00:23:03 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.57.0`" + } + ] + } + }, + { + "version": "7.26.5", + "tag": "@microsoft/api-extractor-model_v7.26.5", + "date": "Thu, 27 Apr 2023 17:18:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.56.0`" + } + ] + } + }, + { + "version": "7.26.4", + "tag": "@microsoft/api-extractor-model_v7.26.4", + "date": "Fri, 10 Feb 2023 01:18:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.2.0`" + } + ] + } + }, + { + "version": "7.26.3", + "tag": "@microsoft/api-extractor-model_v7.26.3", + "date": "Sun, 05 Feb 2023 03:02:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.1`" + } + ] + } + }, + { + "version": "7.26.2", + "tag": "@microsoft/api-extractor-model_v7.26.2", + "date": "Wed, 01 Feb 2023 02:16:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.0`" + } + ] + } + }, + { + "version": "7.26.1", + "tag": "@microsoft/api-extractor-model_v7.26.1", + "date": "Mon, 30 Jan 2023 16:22:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.54.0`" + } + ] + } + }, + { + "version": "7.26.0", + "tag": "@microsoft/api-extractor-model_v7.26.0", + "date": "Wed, 25 Jan 2023 07:26:55 GMT", + "comments": { + "minor": [ + { + "comment": "Add new .api.json field `isAbstract` to track `abstract` modifier in ApiClass, ApiMethod, and ApiProperty via ApiAbstractMixin (GitHub #3661)" + } + ] + } + }, + { + "version": "7.25.3", + "tag": "@microsoft/api-extractor-model_v7.25.3", + "date": "Fri, 09 Dec 2022 16:18:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.3`" + } + ] + } + }, + { + "version": "7.25.2", + "tag": "@microsoft/api-extractor-model_v7.25.2", + "date": "Wed, 26 Oct 2022 00:16:16 GMT", + "comments": { + "patch": [ + { + "comment": "Update the @microsoft/tsdoc dependency version to 0.14.2." + } + ] + } + }, + { + "version": "7.25.1", + "tag": "@microsoft/api-extractor-model_v7.25.1", + "date": "Thu, 13 Oct 2022 00:20:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.2`" + } + ] + } + }, + { + "version": "7.25.0", + "tag": "@microsoft/api-extractor-model_v7.25.0", + "date": "Tue, 11 Oct 2022 23:49:12 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new fileUrlPath property to relevant API items and serialize this to the .api.json. Additionally, add a SourceFile helper class for constructing file URLs from these paths and the projectFolderUrl." + } + ] + } + }, + { + "version": "7.24.4", + "tag": "@microsoft/api-extractor-model_v7.24.4", + "date": "Mon, 10 Oct 2022 15:23:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.1.1`" + } + ] + } + }, + { + "version": "7.24.3", + "tag": "@microsoft/api-extractor-model_v7.24.3", + "date": "Thu, 29 Sep 2022 07:13:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.1.0`" + } + ] + } + }, + { + "version": "7.24.2", + "tag": "@microsoft/api-extractor-model_v7.24.2", + "date": "Wed, 21 Sep 2022 20:21:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.52.0`" + } + ] + } + }, + { + "version": "7.24.1", + "tag": "@microsoft/api-extractor-model_v7.24.1", + "date": "Thu, 15 Sep 2022 00:18:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.0.1`" + } + ] + } + }, + { + "version": "7.24.0", + "tag": "@microsoft/api-extractor-model_v7.24.0", + "date": "Fri, 02 Sep 2022 17:48:42 GMT", + "comments": { + "minor": [ + { + "comment": "Add new ApiExportedMixin mixin class for determining whether an API item is exported or not" + } + ] + } + }, + { + "version": "7.23.3", + "tag": "@microsoft/api-extractor-model_v7.23.3", + "date": "Wed, 24 Aug 2022 03:01:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.1`" + } + ] + } + }, + { + "version": "7.23.2", + "tag": "@microsoft/api-extractor-model_v7.23.2", + "date": "Wed, 24 Aug 2022 00:14:38 GMT", + "comments": { + "patch": [ + { + "comment": "Remove use of LegacyAdapters.sortStable" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.0`" + } + ] + } + }, + { + "version": "7.23.1", + "tag": "@microsoft/api-extractor-model_v7.23.1", + "date": "Fri, 19 Aug 2022 00:17:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.2`" + } + ] + } + }, + { + "version": "7.23.0", + "tag": "@microsoft/api-extractor-model_v7.23.0", + "date": "Wed, 03 Aug 2022 18:40:35 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade TypeScript dependency to 4.7" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.0.0`" + } + ] + } + }, + { + "version": "7.22.2", + "tag": "@microsoft/api-extractor-model_v7.22.2", + "date": "Mon, 01 Aug 2022 02:45:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.0`" + } + ] + } + }, + { + "version": "7.22.1", + "tag": "@microsoft/api-extractor-model_v7.22.1", + "date": "Thu, 21 Jul 2022 23:30:27 GMT", + "comments": { + "patch": [ + { + "comment": "Improve IFindApiItemMessage and fix two small bugs with ApiItemContainerMixin.findMembersWithInheritance()" + } + ] + } + }, + { + "version": "7.22.0", + "tag": "@microsoft/api-extractor-model_v7.22.0", + "date": "Thu, 21 Jul 2022 00:16:14 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new ApiItemContainerMixin.findMembersWithInheritance() method for finding an item's inherited members" + } + ] + } + }, + { + "version": "7.21.0", + "tag": "@microsoft/api-extractor-model_v7.21.0", + "date": "Thu, 30 Jun 2022 04:48:53 GMT", + "comments": { + "minor": [ + { + "comment": "Update model to reflect that index signatures can also be readonly" + } + ] + } + }, + { + "version": "7.20.3", + "tag": "@microsoft/api-extractor-model_v7.20.3", + "date": "Tue, 28 Jun 2022 22:47:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.49.0`" + } + ] + } + }, + { + "version": "7.20.2", + "tag": "@microsoft/api-extractor-model_v7.20.2", + "date": "Tue, 28 Jun 2022 00:23:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.48.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.2`" + } + ] + } + }, + { + "version": "7.20.1", + "tag": "@microsoft/api-extractor-model_v7.20.1", + "date": "Mon, 27 Jun 2022 18:43:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.47.0`" + } + ] + } + }, + { + "version": "7.20.0", + "tag": "@microsoft/api-extractor-model_v7.20.0", + "date": "Sat, 25 Jun 2022 21:00:40 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new initializerTokenRange field to ApiProperty and ApiVariable items." + } + ] + } + }, + { + "version": "7.19.1", + "tag": "@microsoft/api-extractor-model_v7.19.1", + "date": "Sat, 25 Jun 2022 01:54:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.46.0`" + } + ] + } + }, + { + "version": "7.19.0", + "tag": "@microsoft/api-extractor-model_v7.19.0", + "date": "Fri, 24 Jun 2022 07:16:47 GMT", + "comments": { + "minor": [ + { + "comment": "Added new configuration for ItemContainerMixin member ordering" + } + ] + } + }, + { + "version": "7.18.2", + "tag": "@microsoft/api-extractor-model_v7.18.2", + "date": "Fri, 17 Jun 2022 09:17:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.7`" + } + ] + } + }, + { + "version": "7.18.1", + "tag": "@microsoft/api-extractor-model_v7.18.1", + "date": "Fri, 17 Jun 2022 00:16:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.1`" + } + ] + } + }, + { + "version": "7.18.0", + "tag": "@microsoft/api-extractor-model_v7.18.0", + "date": "Tue, 07 Jun 2022 09:37:04 GMT", + "comments": { + "minor": [ + { + "comment": "Add an \"isReadonly\" field to ApiProperty, ApiPropertySignature, and ApiVariable" + }, + { + "comment": "Add an \"isProtected\" field to ApiConstructor, ApiMethod, and ApiProperty" + } + ] + } + }, + { + "version": "7.17.3", + "tag": "@microsoft/api-extractor-model_v7.17.3", + "date": "Tue, 10 May 2022 01:20:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.5`" + } + ] + } + }, + { + "version": "7.17.2", + "tag": "@microsoft/api-extractor-model_v7.17.2", + "date": "Sat, 23 Apr 2022 02:13:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.0`" + } + ] + } + }, + { + "version": "7.17.1", + "tag": "@microsoft/api-extractor-model_v7.17.1", + "date": "Fri, 15 Apr 2022 00:12:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.4`" + } + ] + } + }, + { + "version": "7.17.0", + "tag": "@microsoft/api-extractor-model_v7.17.0", + "date": "Wed, 13 Apr 2022 15:12:40 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new isOptional property to TypeParameters deserialized from the .api.json file with api-extractor-model." + } + ] + } + }, + { + "version": "7.16.2", + "tag": "@microsoft/api-extractor-model_v7.16.2", + "date": "Tue, 12 Apr 2022 02:58:32 GMT", + "comments": { + "patch": [ + { + "comment": "Update TSDoc dependencies." + } + ] + } + }, + { + "version": "7.16.1", + "tag": "@microsoft/api-extractor-model_v7.16.1", + "date": "Sat, 09 Apr 2022 02:24:26 GMT", + "comments": { + "patch": [ + { + "comment": "Rename the \"master\" branch to \"main\"." + }, + { + "comment": "Update a path in the README." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.3`" + } + ] + } + }, + { + "version": "7.16.0", + "tag": "@microsoft/api-extractor-model_v7.16.0", + "date": "Thu, 31 Mar 2022 02:06:05 GMT", + "comments": { + "minor": [ + { + "comment": "Updated api-extractor-model to store whether a parameter is optional." + } + ] + } + }, + { + "version": "7.15.4", + "tag": "@microsoft/api-extractor-model_v7.15.4", + "date": "Tue, 15 Mar 2022 19:15:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.2`" + } + ] + } + }, + { + "version": "7.15.3", + "tag": "@microsoft/api-extractor-model_v7.15.3", + "date": "Wed, 05 Jan 2022 16:07:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.0`" + } + ] + } + }, + { + "version": "7.15.2", + "tag": "@microsoft/api-extractor-model_v7.15.2", + "date": "Mon, 27 Dec 2021 16:10:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.1`" + } + ] + } + }, + { + "version": "7.15.1", + "tag": "@microsoft/api-extractor-model_v7.15.1", + "date": "Thu, 09 Dec 2021 20:34:41 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.2`" + } + ] + } + }, + { + "version": "7.15.0", + "tag": "@microsoft/api-extractor-model_v7.15.0", + "date": "Thu, 09 Dec 2021 00:21:54 GMT", + "comments": { + "minor": [ + { + "comment": "Replace const enums with conventional enums to allow for compatibility with JavaScript consumers." + } + ] + } + }, + { + "version": "7.14.0", + "tag": "@microsoft/api-extractor-model_v7.14.0", + "date": "Wed, 08 Dec 2021 16:14:05 GMT", + "comments": { + "minor": [ + { + "comment": "Update to TypeScript 4.5" + } + ] + } + }, + { + "version": "7.13.18", + "tag": "@microsoft/api-extractor-model_v7.13.18", + "date": "Mon, 06 Dec 2021 16:08:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.0`" + } + ] + } + }, + { + "version": "7.13.17", + "tag": "@microsoft/api-extractor-model_v7.13.17", + "date": "Fri, 03 Dec 2021 03:05:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.0`" + } + ] + } + }, + { + "version": "7.13.16", + "tag": "@microsoft/api-extractor-model_v7.13.16", + "date": "Sat, 06 Nov 2021 00:09:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.2`" + } + ] + } + }, + { + "version": "7.13.15", + "tag": "@microsoft/api-extractor-model_v7.13.15", + "date": "Fri, 05 Nov 2021 15:09:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.5`" + } + ] + } + }, + { + "version": "7.13.14", + "tag": "@microsoft/api-extractor-model_v7.13.14", + "date": "Wed, 27 Oct 2021 00:08:15 GMT", + "comments": { + "patch": [ + { + "comment": "Update the package.json repository field to include the directory property." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.4`" + } + ] + } + }, + { + "version": "7.13.13", + "tag": "@microsoft/api-extractor-model_v7.13.13", + "date": "Wed, 13 Oct 2021 15:09:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.3`" + } + ] + } + }, + { + "version": "7.13.12", + "tag": "@microsoft/api-extractor-model_v7.13.12", + "date": "Fri, 08 Oct 2021 08:08:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.2`" + } + ] + } + }, + { + "version": "7.13.11", + "tag": "@microsoft/api-extractor-model_v7.13.11", + "date": "Thu, 07 Oct 2021 07:13:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.2`" + } + ] + } + }, + { + "version": "7.13.10", + "tag": "@microsoft/api-extractor-model_v7.13.10", + "date": "Tue, 05 Oct 2021 15:08:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.0`" + } + ] + } + }, + { + "version": "7.13.9", + "tag": "@microsoft/api-extractor-model_v7.13.9", + "date": "Fri, 24 Sep 2021 00:09:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.41.0`" + } + ] + } + }, + { + "version": "7.13.8", + "tag": "@microsoft/api-extractor-model_v7.13.8", + "date": "Thu, 23 Sep 2021 00:10:40 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade the `@types/node` dependency to version to version 12." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.1`" + } + ] + } + }, + { + "version": "7.13.7", + "tag": "@microsoft/api-extractor-model_v7.13.7", + "date": "Tue, 14 Sep 2021 01:17:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.2`" + } + ] + } + }, + { + "version": "7.13.6", + "tag": "@microsoft/api-extractor-model_v7.13.6", + "date": "Mon, 13 Sep 2021 15:07:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.1`" + } + ] + } + }, + { + "version": "7.13.5", + "tag": "@microsoft/api-extractor-model_v7.13.5", + "date": "Wed, 11 Aug 2021 00:07:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.0`" + } + ] + } + }, + { + "version": "7.13.4", + "tag": "@microsoft/api-extractor-model_v7.13.4", + "date": "Mon, 12 Jul 2021 23:08:26 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.39.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.0`" + } + ] + } + }, + { + "version": "7.13.3", + "tag": "@microsoft/api-extractor-model_v7.13.3", + "date": "Fri, 04 Jun 2021 19:59:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.39.0`" + } + ] + } + }, + { + "version": "7.13.2", + "tag": "@microsoft/api-extractor-model_v7.13.2", + "date": "Wed, 19 May 2021 00:11:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.38.0`" + } + ] + } + }, + { + "version": "7.13.1", + "tag": "@microsoft/api-extractor-model_v7.13.1", + "date": "Mon, 03 May 2021 15:10:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.37.0`" + } + ] + } + }, + { + "version": "7.13.0", + "tag": "@microsoft/api-extractor-model_v7.13.0", + "date": "Tue, 20 Apr 2021 04:59:51 GMT", + "comments": { + "minor": [ + { + "comment": "The .api.json file format now stores the TSDoc configuration used for parsing doc comments" + } + ] + } + }, + { + "version": "7.12.5", + "tag": "@microsoft/api-extractor-model_v7.12.5", + "date": "Mon, 12 Apr 2021 15:10:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.4`" + } + ] + } + }, + { + "version": "7.12.4", + "tag": "@microsoft/api-extractor-model_v7.12.4", + "date": "Thu, 08 Apr 2021 06:05:31 GMT", + "comments": { + "patch": [ + { + "comment": "Fix minor typo in README.md" + } + ] + } + }, + { + "version": "7.12.3", + "tag": "@microsoft/api-extractor-model_v7.12.3", + "date": "Tue, 06 Apr 2021 15:14:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.3`" + } + ] + } + }, + { + "version": "7.12.2", + "tag": "@microsoft/api-extractor-model_v7.12.2", + "date": "Fri, 05 Feb 2021 16:10:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.0`" + } + ] + } + }, + { + "version": "7.12.1", + "tag": "@microsoft/api-extractor-model_v7.12.1", + "date": "Thu, 10 Dec 2020 23:25:49 GMT", + "comments": { + "patch": [ + { + "comment": "Enable support for @decorator" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.2`" + } + ] + } + }, + { + "version": "7.12.0", + "tag": "@microsoft/api-extractor-model_v7.12.0", + "date": "Wed, 18 Nov 2020 08:19:54 GMT", + "comments": { + "minor": [ + { + "comment": "Introduce an ApiOptionalMixin base class for representing optional properties and methods" + } + ] + } + }, + { + "version": "7.11.0", + "tag": "@microsoft/api-extractor-model_v7.11.0", + "date": "Wed, 18 Nov 2020 06:21:57 GMT", + "comments": { + "minor": [ + { + "comment": "Update .api.json file format to store a new field \"isOptional\" for documenting optional properties" + } + ] + } + }, + { + "version": "7.10.10", + "tag": "@microsoft/api-extractor-model_v7.10.10", + "date": "Wed, 11 Nov 2020 01:08:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.1`" + } + ] + } + }, + { + "version": "7.10.9", + "tag": "@microsoft/api-extractor-model_v7.10.9", + "date": "Tue, 10 Nov 2020 23:13:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.0`" + } + ] + } + }, + { + "version": "7.10.8", + "tag": "@microsoft/api-extractor-model_v7.10.8", + "date": "Fri, 30 Oct 2020 06:38:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.0`" + } + ] + } + }, + { + "version": "7.10.7", + "tag": "@microsoft/api-extractor-model_v7.10.7", + "date": "Fri, 30 Oct 2020 00:10:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.3`" + } + ] + } + }, + { + "version": "7.10.6", + "tag": "@microsoft/api-extractor-model_v7.10.6", + "date": "Thu, 29 Oct 2020 06:14:19 GMT", + "comments": { + "patch": [ + { + "comment": "Fix .d.ts error when the library is imported by a project using TypeScript 4.0" + } + ] + } + }, + { + "version": "7.10.5", + "tag": "@microsoft/api-extractor-model_v7.10.5", + "date": "Wed, 28 Oct 2020 01:18:03 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.2`" + } + ] + } + }, + { + "version": "7.10.4", + "tag": "@microsoft/api-extractor-model_v7.10.4", + "date": "Tue, 27 Oct 2020 15:10:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.4`" + } + ] + } + }, + { + "version": "7.10.3", + "tag": "@microsoft/api-extractor-model_v7.10.3", + "date": "Tue, 06 Oct 2020 00:24:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.1`" + } + ] + } + }, + { + "version": "7.10.2", + "tag": "@microsoft/api-extractor-model_v7.10.2", + "date": "Mon, 05 Oct 2020 22:36:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.0`" + } + ] + } + }, + { + "version": "7.10.1", + "tag": "@microsoft/api-extractor-model_v7.10.1", + "date": "Wed, 30 Sep 2020 18:39:17 GMT", + "comments": { + "patch": [ + { + "comment": "Update to build with @rushstack/heft-node-rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.3`" + } + ] + } + }, + { + "version": "7.10.0", + "tag": "@microsoft/api-extractor-model_v7.10.0", + "date": "Wed, 30 Sep 2020 06:53:53 GMT", + "comments": { + "patch": [ + { + "comment": "Update README.md" + } + ], + "minor": [ + { + "comment": "Upgrade compiler; the API now requires TypeScript 3.9 or newer" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.2`" + } + ] + } + }, + { + "version": "7.9.7", + "tag": "@microsoft/api-extractor-model_v7.9.7", + "date": "Tue, 22 Sep 2020 05:45:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.1`" + } + ] + } + }, + { + "version": "7.9.6", + "tag": "@microsoft/api-extractor-model_v7.9.6", + "date": "Tue, 22 Sep 2020 01:45:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.0`" + } + ] + } + }, + { + "version": "7.9.5", + "tag": "@microsoft/api-extractor-model_v7.9.5", + "date": "Tue, 22 Sep 2020 00:08:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.0.0`" + } + ] + } + }, + { + "version": "7.9.4", + "tag": "@microsoft/api-extractor-model_v7.9.4", + "date": "Sat, 19 Sep 2020 04:37:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.2`" + } + ] + } + }, + { + "version": "7.9.3", + "tag": "@microsoft/api-extractor-model_v7.9.3", + "date": "Sat, 19 Sep 2020 03:33:07 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.1`" + } + ] + } + }, + { + "version": "7.9.2", + "tag": "@microsoft/api-extractor-model_v7.9.2", + "date": "Fri, 18 Sep 2020 22:57:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.0`" + } + ] + } + }, + { + "version": "7.9.1", + "tag": "@microsoft/api-extractor-model_v7.9.1", + "date": "Fri, 18 Sep 2020 21:49:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.0`" + } + ] + } + }, + { + "version": "7.9.0", + "tag": "@microsoft/api-extractor-model_v7.9.0", + "date": "Sun, 13 Sep 2020 01:53:20 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for system selectors in declaration references" + } + ] + } + }, + { + "version": "7.8.22", + "tag": "@microsoft/api-extractor-model_v7.8.22", + "date": "Fri, 11 Sep 2020 02:13:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.32.0`" + } + ] + } + }, + { + "version": "7.8.21", + "tag": "@microsoft/api-extractor-model_v7.8.21", + "date": "Mon, 07 Sep 2020 07:37:37 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.31.0`" + } + ] + } + }, + { + "version": "7.8.20", + "tag": "@microsoft/api-extractor-model_v7.8.20", + "date": "Sat, 05 Sep 2020 18:56:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix \"Converting circular structure to JSON\" error (GitHub #2152)" + } + ] + } + }, + { + "version": "7.8.19", + "tag": "@microsoft/api-extractor-model_v7.8.19", + "date": "Thu, 27 Aug 2020 11:27:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.30.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.3.0`" + } + ] + } + }, + { + "version": "7.8.18", + "tag": "@microsoft/api-extractor-model_v7.8.18", + "date": "Mon, 24 Aug 2020 07:35:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.29.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.2.1`" + } + ] + } + }, + { + "version": "7.8.17", + "tag": "@microsoft/api-extractor-model_v7.8.17", + "date": "Sat, 22 Aug 2020 05:55:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.29.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.2.0`" + } + ] + } + }, + { + "version": "7.8.16", + "tag": "@microsoft/api-extractor-model_v7.8.16", + "date": "Tue, 18 Aug 2020 23:59:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.28.0`" + } + ] + } + }, + { + "version": "7.8.15", + "tag": "@microsoft/api-extractor-model_v7.8.15", + "date": "Mon, 17 Aug 2020 04:53:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.27.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.1.0`" + } + ] + } + }, + { + "version": "7.8.14", + "tag": "@microsoft/api-extractor-model_v7.8.14", + "date": "Wed, 12 Aug 2020 00:10:05 GMT", + "comments": { + "patch": [ + { + "comment": "Updated project to build with Heft" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.26.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.0.4`" + } + ] + } + }, + { + "version": "7.8.13", + "tag": "@microsoft/api-extractor-model_v7.8.13", + "date": "Wed, 05 Aug 2020 18:27:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.26.1`" + } + ] + } + }, + { + "version": "7.8.12", + "tag": "@microsoft/api-extractor-model_v7.8.12", + "date": "Fri, 03 Jul 2020 15:09:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.4` to `3.25.0`" + } + ] + } + }, + { + "version": "7.8.11", + "tag": "@microsoft/api-extractor-model_v7.8.11", + "date": "Thu, 25 Jun 2020 06:43:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.3` to `3.24.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `1.0.1` to `1.0.2`" + } + ] + } + }, + { + "version": "7.8.10", + "tag": "@microsoft/api-extractor-model_v7.8.10", + "date": "Wed, 24 Jun 2020 09:50:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.2` to `3.24.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `1.0.0` to `1.0.1`" + } + ] + } + }, + { + "version": "7.8.9", + "tag": "@microsoft/api-extractor-model_v7.8.9", + "date": "Wed, 24 Jun 2020 09:04:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.1` to `3.24.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.8` to `1.0.0`" + } + ] + } + }, + { + "version": "7.8.8", + "tag": "@microsoft/api-extractor-model_v7.8.8", + "date": "Wed, 10 Jun 2020 20:48:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.0` to `3.24.1`" + } + ] + } + }, + { + "version": "7.8.7", + "tag": "@microsoft/api-extractor-model_v7.8.7", + "date": "Sat, 30 May 2020 02:59:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.23.1` to `3.24.0`" + } + ] + } + }, + { + "version": "7.8.6", + "tag": "@microsoft/api-extractor-model_v7.8.6", + "date": "Thu, 28 May 2020 05:59:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.23.0` to `3.23.1`" + } + ] + } + }, + { + "version": "7.8.5", + "tag": "@microsoft/api-extractor-model_v7.8.5", + "date": "Wed, 27 May 2020 05:15:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.22.1` to `3.23.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.7` to `0.5.8`" + } + ] + } + }, + { + "version": "7.8.4", + "tag": "@microsoft/api-extractor-model_v7.8.4", + "date": "Tue, 26 May 2020 23:00:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.22.0` to `3.22.1`" + } + ] + } + }, + { + "version": "7.8.3", + "tag": "@microsoft/api-extractor-model_v7.8.3", + "date": "Fri, 22 May 2020 15:08:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.21.0` to `3.22.0`" + } + ] + } + }, + { + "version": "7.8.2", + "tag": "@microsoft/api-extractor-model_v7.8.2", + "date": "Thu, 21 May 2020 23:09:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.20.0` to `3.21.0`" + } + ] + } + }, + { + "version": "7.8.1", + "tag": "@microsoft/api-extractor-model_v7.8.1", + "date": "Thu, 21 May 2020 15:42:00 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.7` to `3.20.0`" + } + ] + } + }, + { + "version": "7.8.0", + "tag": "@microsoft/api-extractor-model_v7.8.0", + "date": "Wed, 06 May 2020 08:23:45 GMT", + "comments": { + "minor": [ + { + "comment": "Enable canonicalReference to ApiItem lookup" + } + ] + } + }, + { + "version": "7.7.11", + "tag": "@microsoft/api-extractor-model_v7.7.11", + "date": "Wed, 08 Apr 2020 04:07:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.6` to `3.19.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.6` to `0.5.7`" + } + ] + } + }, + { + "version": "7.7.10", + "tag": "@microsoft/api-extractor-model_v7.7.10", + "date": "Sat, 28 Mar 2020 00:37:16 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to TSdoc 0.12.19" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.5` to `3.19.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.5` to `0.5.6`" + } + ] + } + }, + { + "version": "7.7.9", + "tag": "@microsoft/api-extractor-model_v7.7.9", + "date": "Wed, 18 Mar 2020 15:07:47 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade cyclic dependencies" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.4` to `3.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.4` to `0.5.5`" + } + ] + } + }, + { + "version": "7.7.8", + "tag": "@microsoft/api-extractor-model_v7.7.8", + "date": "Tue, 17 Mar 2020 23:55:58 GMT", + "comments": { + "patch": [ + { + "comment": "Replace dependencies whose NPM scope was renamed from `@microsoft` to `@rushstack`" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.3` to `3.19.4`" + } + ] + } + }, + { + "version": "7.7.7", + "tag": "@microsoft/api-extractor-model_v7.7.7", + "date": "Tue, 28 Jan 2020 02:23:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.2` to `3.19.3`" + } + ] + } + }, + { + "version": "7.7.6", + "tag": "@microsoft/api-extractor-model_v7.7.6", + "date": "Thu, 23 Jan 2020 01:07:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.1` to `3.19.2`" + } + ] + } + }, + { + "version": "7.7.5", + "tag": "@microsoft/api-extractor-model_v7.7.5", + "date": "Tue, 21 Jan 2020 21:56:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.0` to `3.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.3` to `0.5.4`" + } + ] + } + }, + { + "version": "7.7.4", + "tag": "@microsoft/api-extractor-model_v7.7.4", + "date": "Sun, 19 Jan 2020 02:26:52 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade Node typings to Node 10" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.3` to `3.19.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.2` to `0.5.3`" + } + ] + } + }, + { + "version": "7.7.3", + "tag": "@microsoft/api-extractor-model_v7.7.3", + "date": "Fri, 17 Jan 2020 01:08:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.2` to `3.18.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.1` to `0.5.2`" + } + ] + } + }, + { + "version": "7.7.2", + "tag": "@microsoft/api-extractor-model_v7.7.2", + "date": "Thu, 09 Jan 2020 06:44:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.1` to `3.18.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.0` to `0.5.1`" + } + ] + } + }, + { + "version": "7.7.1", + "tag": "@microsoft/api-extractor-model_v7.7.1", + "date": "Wed, 08 Jan 2020 00:11:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.0` to `3.18.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.2` to `0.5.0`" + } + ] + } + }, + { + "version": "7.7.0", + "tag": "@microsoft/api-extractor-model_v7.7.0", + "date": "Tue, 03 Dec 2019 03:17:43 GMT", + "comments": { + "minor": [ + { + "comment": "Improve declaration reference syntax to allow linking to overloaded functions/methods" + } + ] + } + }, + { + "version": "7.6.0", + "tag": "@microsoft/api-extractor-model_v7.6.0", + "date": "Sun, 24 Nov 2019 00:54:04 GMT", + "comments": { + "minor": [ + { + "comment": "Added support for `@throws`" + } + ] + } + }, + { + "version": "7.5.6", + "tag": "@microsoft/api-extractor-model_v7.5.6", + "date": "Fri, 15 Nov 2019 04:50:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.17.1` to `3.18.0`" + } + ] + } + }, + { + "version": "7.5.5", + "tag": "@microsoft/api-extractor-model_v7.5.5", + "date": "Mon, 11 Nov 2019 16:07:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.17.0` to `3.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.1` to `0.4.2`" + } + ] + } + }, + { + "version": "7.5.4", + "tag": "@microsoft/api-extractor-model_v7.5.4", + "date": "Tue, 05 Nov 2019 06:49:28 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where API reports sometimes were ordered differently depending on the version of NodeJS (GitHub #1552)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.16.0` to `3.17.0`" + } + ] + } + }, + { + "version": "7.5.3", + "tag": "@microsoft/api-extractor-model_v7.5.3", + "date": "Tue, 05 Nov 2019 01:08:39 GMT", + "comments": { + "patch": [ + { + "comment": "Clarified an error message" + } + ] + } + }, + { + "version": "7.5.2", + "tag": "@microsoft/api-extractor-model_v7.5.2", + "date": "Tue, 22 Oct 2019 06:24:44 GMT", + "comments": { + "patch": [ + { + "comment": "Refactor some code as part of migration from TSLint to ESLint" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.15.1` to `3.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.0` to `0.4.1`" + } + ] + } + }, + { + "version": "7.5.1", + "tag": "@microsoft/api-extractor-model_v7.5.1", + "date": "Sun, 29 Sep 2019 23:56:29 GMT", + "comments": { + "patch": [ + { + "comment": "Update repository URL" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.15.0` to `3.15.1`" + } + ] + } + }, + { + "version": "7.5.0", + "tag": "@microsoft/api-extractor-model_v7.5.0", + "date": "Wed, 25 Sep 2019 15:15:31 GMT", + "comments": { + "minor": [ + { + "comment": "Add ApiItem.getMergedSiblings() API" + } + ] + } + }, + { + "version": "7.4.2", + "tag": "@microsoft/api-extractor-model_v7.4.2", + "date": "Mon, 23 Sep 2019 15:14:55 GMT", + "comments": { + "patch": [ + { + "comment": "Remove unnecessary dependency on @types/node" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.2` to `3.15.0`" + } + ] + } + }, + { + "version": "7.4.1", + "tag": "@microsoft/api-extractor-model_v7.4.1", + "date": "Tue, 10 Sep 2019 22:32:23 GMT", + "comments": { + "patch": [ + { + "comment": "Update documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.1` to `3.14.2`" + } + ] + } + }, + { + "version": "7.4.0", + "tag": "@microsoft/api-extractor-model_v7.4.0", + "date": "Tue, 10 Sep 2019 20:38:33 GMT", + "comments": { + "minor": [ + { + "comment": "Add 'canonicalReference' to ExcerptToken" + } + ] + } + }, + { + "version": "7.3.4", + "tag": "@microsoft/api-extractor-model_v7.3.4", + "date": "Wed, 04 Sep 2019 18:28:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.0` to `3.14.1`" + } + ] + } + }, + { + "version": "7.3.3", + "tag": "@microsoft/api-extractor-model_v7.3.3", + "date": "Wed, 04 Sep 2019 15:15:37 GMT", + "comments": { + "patch": [ + { + "comment": "Update TSDoc dependency to 0.12.14" + } + ] + } + }, + { + "version": "7.3.2", + "tag": "@microsoft/api-extractor-model_v7.3.2", + "date": "Thu, 08 Aug 2019 15:14:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.13.0` to `3.14.0`" + } + ] + } + }, + { + "version": "7.3.1", + "tag": "@microsoft/api-extractor-model_v7.3.1", + "date": "Thu, 08 Aug 2019 00:49:05 GMT", + "comments": { + "patch": [ + { + "comment": "(Experimental) Add ApiExtractor.canonicalReference which is a beta implementation of the revised TSDoc declaration reference notation" + } + ] + } + }, + { + "version": "7.3.0", + "tag": "@microsoft/api-extractor-model_v7.3.0", + "date": "Mon, 22 Jul 2019 19:13:10 GMT", + "comments": { + "minor": [ + { + "comment": "Rename `ApiItem.canonicalReference` to `.containerKey`; rename `ApiItemContainerMixin.tryGetMember()` to `.tryGetMemberByKey()`; rename `Api___.getCanonicalReference()` to `.getContainerKey()`" + } + ] + } + }, + { + "version": "7.2.0", + "tag": "@microsoft/api-extractor-model_v7.2.0", + "date": "Tue, 11 Jun 2019 00:48:06 GMT", + "comments": { + "patch": [ + { + "comment": "Improve the .api.json deserializer to validate the schema version and support backwards compatibility" + } + ], + "minor": [ + { + "comment": "Add API support for type parameters and type alias types" + } + ] + } + }, + { + "version": "7.1.3", + "tag": "@microsoft/api-extractor-model_v7.1.3", + "date": "Wed, 05 Jun 2019 19:12:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where TSDoc index selectors (ApiParameterListMixin.overloadIndex) started from 0, whereas TSDoc requires a nonzero number" + } + ] + } + }, + { + "version": "7.1.2", + "tag": "@microsoft/api-extractor-model_v7.1.2", + "date": "Tue, 04 Jun 2019 05:51:53 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where ApiConstructor inherited from ApiStaticMixin, but TypeScript constructors cannot be static" + } + ] + } + }, + { + "version": "7.1.1", + "tag": "@microsoft/api-extractor-model_v7.1.1", + "date": "Mon, 27 May 2019 04:13:44 GMT", + "comments": { + "patch": [ + { + "comment": "Make the strings returned by ApiItem.displayName less verbose" + }, + { + "comment": "Improve formatting of the strings returned by ApiItem.getScopedNameWithinPackage()" + } + ] + } + }, + { + "version": "7.1.0", + "tag": "@microsoft/api-extractor-model_v7.1.0", + "date": "Tue, 16 Apr 2019 11:01:37 GMT", + "comments": { + "minor": [ + { + "comment": "Initial stable release of API Extractor 7" + } + ] + } + }, + { + "version": "7.0.28", + "tag": "@microsoft/api-extractor-model_v7.0.28", + "date": "Wed, 20 Mar 2019 19:14:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.12.1` to `3.13.0`" + } + ] + } + }, + { + "version": "7.0.27", + "tag": "@microsoft/api-extractor-model_v7.0.27", + "date": "Mon, 18 Mar 2019 04:28:43 GMT", + "comments": { + "patch": [ + { + "comment": "Add helper functions for ReleaseTag" + }, + { + "comment": "Export IApiItemConstructor to eliminate the ae-forgotten-export warning" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.12.0` to `3.12.1`" + } + ] + } + }, + { + "version": "7.0.26", + "tag": "@microsoft/api-extractor-model_v7.0.26", + "date": "Wed, 13 Mar 2019 19:13:14 GMT", + "comments": { + "patch": [ + { + "comment": "Refactor code to move the IndentedWriter API from api-extractor-model to api-documenter" + } + ] + } + }, + { + "version": "7.0.25", + "tag": "@microsoft/api-extractor-model_v7.0.25", + "date": "Wed, 13 Mar 2019 01:14:05 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade TSDoc" + } + ] + } + }, + { + "version": "7.0.24", + "tag": "@microsoft/api-extractor-model_v7.0.24", + "date": "Mon, 11 Mar 2019 16:13:36 GMT", + "comments": { + "patch": [ + { + "comment": "Initial setup of new package @microsoft/api-extractor-model" + } + ] + } + } + ] +} diff --git a/packages/api-extractor-model/CHANGELOG.md b/packages/api-extractor-model/CHANGELOG.md new file mode 100644 index 000000000..e285bd281 --- /dev/null +++ b/packages/api-extractor-model/CHANGELOG.md @@ -0,0 +1,965 @@ +# Change Log - @microsoft/api-extractor-model + +This log was last generated on Thu, 28 Sep 2023 20:53:16 GMT and should not be manually modified. + +## 7.28.2 +Thu, 28 Sep 2023 20:53:16 GMT + +_Version update only_ + +## 7.28.1 +Tue, 26 Sep 2023 09:30:33 GMT + +### Patches + +- Update type-only imports to include the type modifier. + +## 7.28.0 +Fri, 15 Sep 2023 00:36:58 GMT + +### Minor changes + +- Update @types/node from 14 to 18 + +## 7.27.6 +Tue, 08 Aug 2023 07:10:40 GMT + +_Version update only_ + +## 7.27.5 +Wed, 19 Jul 2023 00:20:32 GMT + +_Version update only_ + +## 7.27.4 +Thu, 06 Jul 2023 00:16:20 GMT + +_Version update only_ + +## 7.27.3 +Thu, 15 Jun 2023 00:21:01 GMT + +_Version update only_ + +## 7.27.2 +Wed, 07 Jun 2023 22:45:16 GMT + +_Version update only_ + +## 7.27.1 +Mon, 29 May 2023 15:21:15 GMT + +_Version update only_ + +## 7.27.0 +Mon, 22 May 2023 06:34:32 GMT + +### Minor changes + +- Upgrade the TypeScript dependency to ~5.0.4 + +## 7.26.9 +Fri, 12 May 2023 00:23:05 GMT + +_Version update only_ + +## 7.26.8 +Thu, 04 May 2023 00:20:28 GMT + +### Patches + +- Fix a mistake in the documentation for ApiParameterListMixin.overloadIndex + +## 7.26.7 +Mon, 01 May 2023 15:23:20 GMT + +_Version update only_ + +## 7.26.6 +Sat, 29 Apr 2023 00:23:03 GMT + +_Version update only_ + +## 7.26.5 +Thu, 27 Apr 2023 17:18:43 GMT + +_Version update only_ + +## 7.26.4 +Fri, 10 Feb 2023 01:18:51 GMT + +_Version update only_ + +## 7.26.3 +Sun, 05 Feb 2023 03:02:02 GMT + +_Version update only_ + +## 7.26.2 +Wed, 01 Feb 2023 02:16:34 GMT + +_Version update only_ + +## 7.26.1 +Mon, 30 Jan 2023 16:22:30 GMT + +_Version update only_ + +## 7.26.0 +Wed, 25 Jan 2023 07:26:55 GMT + +### Minor changes + +- Add new .api.json field `isAbstract` to track `abstract` modifier in ApiClass, ApiMethod, and ApiProperty via ApiAbstractMixin (GitHub #3661) + +## 7.25.3 +Fri, 09 Dec 2022 16:18:28 GMT + +_Version update only_ + +## 7.25.2 +Wed, 26 Oct 2022 00:16:16 GMT + +### Patches + +- Update the @microsoft/tsdoc dependency version to 0.14.2. + +## 7.25.1 +Thu, 13 Oct 2022 00:20:15 GMT + +_Version update only_ + +## 7.25.0 +Tue, 11 Oct 2022 23:49:12 GMT + +### Minor changes + +- Add a new fileUrlPath property to relevant API items and serialize this to the .api.json. Additionally, add a SourceFile helper class for constructing file URLs from these paths and the projectFolderUrl. + +## 7.24.4 +Mon, 10 Oct 2022 15:23:44 GMT + +_Version update only_ + +## 7.24.3 +Thu, 29 Sep 2022 07:13:06 GMT + +_Version update only_ + +## 7.24.2 +Wed, 21 Sep 2022 20:21:10 GMT + +_Version update only_ + +## 7.24.1 +Thu, 15 Sep 2022 00:18:52 GMT + +_Version update only_ + +## 7.24.0 +Fri, 02 Sep 2022 17:48:42 GMT + +### Minor changes + +- Add new ApiExportedMixin mixin class for determining whether an API item is exported or not + +## 7.23.3 +Wed, 24 Aug 2022 03:01:22 GMT + +_Version update only_ + +## 7.23.2 +Wed, 24 Aug 2022 00:14:38 GMT + +### Patches + +- Remove use of LegacyAdapters.sortStable + +## 7.23.1 +Fri, 19 Aug 2022 00:17:19 GMT + +_Version update only_ + +## 7.23.0 +Wed, 03 Aug 2022 18:40:35 GMT + +### Minor changes + +- Upgrade TypeScript dependency to 4.7 + +## 7.22.2 +Mon, 01 Aug 2022 02:45:32 GMT + +_Version update only_ + +## 7.22.1 +Thu, 21 Jul 2022 23:30:27 GMT + +### Patches + +- Improve IFindApiItemMessage and fix two small bugs with ApiItemContainerMixin.findMembersWithInheritance() + +## 7.22.0 +Thu, 21 Jul 2022 00:16:14 GMT + +### Minor changes + +- Add a new ApiItemContainerMixin.findMembersWithInheritance() method for finding an item's inherited members + +## 7.21.0 +Thu, 30 Jun 2022 04:48:53 GMT + +### Minor changes + +- Update model to reflect that index signatures can also be readonly + +## 7.20.3 +Tue, 28 Jun 2022 22:47:13 GMT + +_Version update only_ + +## 7.20.2 +Tue, 28 Jun 2022 00:23:32 GMT + +_Version update only_ + +## 7.20.1 +Mon, 27 Jun 2022 18:43:09 GMT + +_Version update only_ + +## 7.20.0 +Sat, 25 Jun 2022 21:00:40 GMT + +### Minor changes + +- Add a new initializerTokenRange field to ApiProperty and ApiVariable items. + +## 7.19.1 +Sat, 25 Jun 2022 01:54:29 GMT + +_Version update only_ + +## 7.19.0 +Fri, 24 Jun 2022 07:16:47 GMT + +### Minor changes + +- Added new configuration for ItemContainerMixin member ordering + +## 7.18.2 +Fri, 17 Jun 2022 09:17:54 GMT + +_Version update only_ + +## 7.18.1 +Fri, 17 Jun 2022 00:16:18 GMT + +_Version update only_ + +## 7.18.0 +Tue, 07 Jun 2022 09:37:04 GMT + +### Minor changes + +- Add an "isReadonly" field to ApiProperty, ApiPropertySignature, and ApiVariable +- Add an "isProtected" field to ApiConstructor, ApiMethod, and ApiProperty + +## 7.17.3 +Tue, 10 May 2022 01:20:43 GMT + +_Version update only_ + +## 7.17.2 +Sat, 23 Apr 2022 02:13:07 GMT + +_Version update only_ + +## 7.17.1 +Fri, 15 Apr 2022 00:12:36 GMT + +_Version update only_ + +## 7.17.0 +Wed, 13 Apr 2022 15:12:40 GMT + +### Minor changes + +- Add a new isOptional property to TypeParameters deserialized from the .api.json file with api-extractor-model. + +## 7.16.2 +Tue, 12 Apr 2022 02:58:32 GMT + +### Patches + +- Update TSDoc dependencies. + +## 7.16.1 +Sat, 09 Apr 2022 02:24:26 GMT + +### Patches + +- Rename the "master" branch to "main". +- Update a path in the README. + +## 7.16.0 +Thu, 31 Mar 2022 02:06:05 GMT + +### Minor changes + +- Updated api-extractor-model to store whether a parameter is optional. + +## 7.15.4 +Tue, 15 Mar 2022 19:15:53 GMT + +_Version update only_ + +## 7.15.3 +Wed, 05 Jan 2022 16:07:47 GMT + +_Version update only_ + +## 7.15.2 +Mon, 27 Dec 2021 16:10:40 GMT + +_Version update only_ + +## 7.15.1 +Thu, 09 Dec 2021 20:34:41 GMT + +_Version update only_ + +## 7.15.0 +Thu, 09 Dec 2021 00:21:54 GMT + +### Minor changes + +- Replace const enums with conventional enums to allow for compatibility with JavaScript consumers. + +## 7.14.0 +Wed, 08 Dec 2021 16:14:05 GMT + +### Minor changes + +- Update to TypeScript 4.5 + +## 7.13.18 +Mon, 06 Dec 2021 16:08:33 GMT + +_Version update only_ + +## 7.13.17 +Fri, 03 Dec 2021 03:05:22 GMT + +_Version update only_ + +## 7.13.16 +Sat, 06 Nov 2021 00:09:13 GMT + +_Version update only_ + +## 7.13.15 +Fri, 05 Nov 2021 15:09:18 GMT + +_Version update only_ + +## 7.13.14 +Wed, 27 Oct 2021 00:08:15 GMT + +### Patches + +- Update the package.json repository field to include the directory property. + +## 7.13.13 +Wed, 13 Oct 2021 15:09:54 GMT + +_Version update only_ + +## 7.13.12 +Fri, 08 Oct 2021 08:08:34 GMT + +_Version update only_ + +## 7.13.11 +Thu, 07 Oct 2021 07:13:35 GMT + +_Version update only_ + +## 7.13.10 +Tue, 05 Oct 2021 15:08:38 GMT + +_Version update only_ + +## 7.13.9 +Fri, 24 Sep 2021 00:09:29 GMT + +_Version update only_ + +## 7.13.8 +Thu, 23 Sep 2021 00:10:40 GMT + +### Patches + +- Upgrade the `@types/node` dependency to version to version 12. + +## 7.13.7 +Tue, 14 Sep 2021 01:17:04 GMT + +_Version update only_ + +## 7.13.6 +Mon, 13 Sep 2021 15:07:06 GMT + +_Version update only_ + +## 7.13.5 +Wed, 11 Aug 2021 00:07:21 GMT + +_Version update only_ + +## 7.13.4 +Mon, 12 Jul 2021 23:08:26 GMT + +_Version update only_ + +## 7.13.3 +Fri, 04 Jun 2021 19:59:53 GMT + +_Version update only_ + +## 7.13.2 +Wed, 19 May 2021 00:11:39 GMT + +_Version update only_ + +## 7.13.1 +Mon, 03 May 2021 15:10:29 GMT + +_Version update only_ + +## 7.13.0 +Tue, 20 Apr 2021 04:59:51 GMT + +### Minor changes + +- The .api.json file format now stores the TSDoc configuration used for parsing doc comments + +## 7.12.5 +Mon, 12 Apr 2021 15:10:28 GMT + +_Version update only_ + +## 7.12.4 +Thu, 08 Apr 2021 06:05:31 GMT + +### Patches + +- Fix minor typo in README.md + +## 7.12.3 +Tue, 06 Apr 2021 15:14:22 GMT + +_Version update only_ + +## 7.12.2 +Fri, 05 Feb 2021 16:10:42 GMT + +_Version update only_ + +## 7.12.1 +Thu, 10 Dec 2020 23:25:49 GMT + +### Patches + +- Enable support for @decorator + +## 7.12.0 +Wed, 18 Nov 2020 08:19:54 GMT + +### Minor changes + +- Introduce an ApiOptionalMixin base class for representing optional properties and methods + +## 7.11.0 +Wed, 18 Nov 2020 06:21:57 GMT + +### Minor changes + +- Update .api.json file format to store a new field "isOptional" for documenting optional properties + +## 7.10.10 +Wed, 11 Nov 2020 01:08:59 GMT + +_Version update only_ + +## 7.10.9 +Tue, 10 Nov 2020 23:13:12 GMT + +_Version update only_ + +## 7.10.8 +Fri, 30 Oct 2020 06:38:39 GMT + +_Version update only_ + +## 7.10.7 +Fri, 30 Oct 2020 00:10:14 GMT + +_Version update only_ + +## 7.10.6 +Thu, 29 Oct 2020 06:14:19 GMT + +### Patches + +- Fix .d.ts error when the library is imported by a project using TypeScript 4.0 + +## 7.10.5 +Wed, 28 Oct 2020 01:18:03 GMT + +_Version update only_ + +## 7.10.4 +Tue, 27 Oct 2020 15:10:14 GMT + +_Version update only_ + +## 7.10.3 +Tue, 06 Oct 2020 00:24:06 GMT + +_Version update only_ + +## 7.10.2 +Mon, 05 Oct 2020 22:36:57 GMT + +_Version update only_ + +## 7.10.1 +Wed, 30 Sep 2020 18:39:17 GMT + +### Patches + +- Update to build with @rushstack/heft-node-rig + +## 7.10.0 +Wed, 30 Sep 2020 06:53:53 GMT + +### Minor changes + +- Upgrade compiler; the API now requires TypeScript 3.9 or newer + +### Patches + +- Update README.md + +## 7.9.7 +Tue, 22 Sep 2020 05:45:57 GMT + +_Version update only_ + +## 7.9.6 +Tue, 22 Sep 2020 01:45:31 GMT + +_Version update only_ + +## 7.9.5 +Tue, 22 Sep 2020 00:08:53 GMT + +_Version update only_ + +## 7.9.4 +Sat, 19 Sep 2020 04:37:27 GMT + +_Version update only_ + +## 7.9.3 +Sat, 19 Sep 2020 03:33:07 GMT + +_Version update only_ + +## 7.9.2 +Fri, 18 Sep 2020 22:57:24 GMT + +_Version update only_ + +## 7.9.1 +Fri, 18 Sep 2020 21:49:54 GMT + +_Version update only_ + +## 7.9.0 +Sun, 13 Sep 2020 01:53:20 GMT + +### Minor changes + +- Add support for system selectors in declaration references + +## 7.8.22 +Fri, 11 Sep 2020 02:13:35 GMT + +_Version update only_ + +## 7.8.21 +Mon, 07 Sep 2020 07:37:37 GMT + +_Version update only_ + +## 7.8.20 +Sat, 05 Sep 2020 18:56:34 GMT + +### Patches + +- Fix "Converting circular structure to JSON" error (GitHub #2152) + +## 7.8.19 +Thu, 27 Aug 2020 11:27:06 GMT + +_Version update only_ + +## 7.8.18 +Mon, 24 Aug 2020 07:35:20 GMT + +_Version update only_ + +## 7.8.17 +Sat, 22 Aug 2020 05:55:43 GMT + +_Version update only_ + +## 7.8.16 +Tue, 18 Aug 2020 23:59:42 GMT + +_Version update only_ + +## 7.8.15 +Mon, 17 Aug 2020 04:53:23 GMT + +_Version update only_ + +## 7.8.14 +Wed, 12 Aug 2020 00:10:05 GMT + +### Patches + +- Updated project to build with Heft + +## 7.8.13 +Wed, 05 Aug 2020 18:27:33 GMT + +_Version update only_ + +## 7.8.12 +Fri, 03 Jul 2020 15:09:04 GMT + +_Version update only_ + +## 7.8.11 +Thu, 25 Jun 2020 06:43:35 GMT + +_Version update only_ + +## 7.8.10 +Wed, 24 Jun 2020 09:50:48 GMT + +_Version update only_ + +## 7.8.9 +Wed, 24 Jun 2020 09:04:28 GMT + +_Version update only_ + +## 7.8.8 +Wed, 10 Jun 2020 20:48:30 GMT + +_Version update only_ + +## 7.8.7 +Sat, 30 May 2020 02:59:54 GMT + +_Version update only_ + +## 7.8.6 +Thu, 28 May 2020 05:59:02 GMT + +_Version update only_ + +## 7.8.5 +Wed, 27 May 2020 05:15:11 GMT + +_Version update only_ + +## 7.8.4 +Tue, 26 May 2020 23:00:25 GMT + +_Version update only_ + +## 7.8.3 +Fri, 22 May 2020 15:08:43 GMT + +_Version update only_ + +## 7.8.2 +Thu, 21 May 2020 23:09:44 GMT + +_Version update only_ + +## 7.8.1 +Thu, 21 May 2020 15:42:00 GMT + +_Version update only_ + +## 7.8.0 +Wed, 06 May 2020 08:23:45 GMT + +### Minor changes + +- Enable canonicalReference to ApiItem lookup + +## 7.7.11 +Wed, 08 Apr 2020 04:07:33 GMT + +_Version update only_ + +## 7.7.10 +Sat, 28 Mar 2020 00:37:16 GMT + +### Patches + +- Upgrade to TSdoc 0.12.19 + +## 7.7.9 +Wed, 18 Mar 2020 15:07:47 GMT + +### Patches + +- Upgrade cyclic dependencies + +## 7.7.8 +Tue, 17 Mar 2020 23:55:58 GMT + +### Patches + +- Replace dependencies whose NPM scope was renamed from `@microsoft` to `@rushstack` + +## 7.7.7 +Tue, 28 Jan 2020 02:23:44 GMT + +_Version update only_ + +## 7.7.6 +Thu, 23 Jan 2020 01:07:56 GMT + +_Version update only_ + +## 7.7.5 +Tue, 21 Jan 2020 21:56:14 GMT + +_Version update only_ + +## 7.7.4 +Sun, 19 Jan 2020 02:26:52 GMT + +### Patches + +- Upgrade Node typings to Node 10 + +## 7.7.3 +Fri, 17 Jan 2020 01:08:23 GMT + +_Version update only_ + +## 7.7.2 +Thu, 09 Jan 2020 06:44:13 GMT + +_Version update only_ + +## 7.7.1 +Wed, 08 Jan 2020 00:11:31 GMT + +_Version update only_ + +## 7.7.0 +Tue, 03 Dec 2019 03:17:43 GMT + +### Minor changes + +- Improve declaration reference syntax to allow linking to overloaded functions/methods + +## 7.6.0 +Sun, 24 Nov 2019 00:54:04 GMT + +### Minor changes + +- Added support for `@throws` + +## 7.5.6 +Fri, 15 Nov 2019 04:50:50 GMT + +_Version update only_ + +## 7.5.5 +Mon, 11 Nov 2019 16:07:56 GMT + +_Version update only_ + +## 7.5.4 +Tue, 05 Nov 2019 06:49:28 GMT + +### Patches + +- Fix an issue where API reports sometimes were ordered differently depending on the version of NodeJS (GitHub #1552) + +## 7.5.3 +Tue, 05 Nov 2019 01:08:39 GMT + +### Patches + +- Clarified an error message + +## 7.5.2 +Tue, 22 Oct 2019 06:24:44 GMT + +### Patches + +- Refactor some code as part of migration from TSLint to ESLint + +## 7.5.1 +Sun, 29 Sep 2019 23:56:29 GMT + +### Patches + +- Update repository URL + +## 7.5.0 +Wed, 25 Sep 2019 15:15:31 GMT + +### Minor changes + +- Add ApiItem.getMergedSiblings() API + +## 7.4.2 +Mon, 23 Sep 2019 15:14:55 GMT + +### Patches + +- Remove unnecessary dependency on @types/node + +## 7.4.1 +Tue, 10 Sep 2019 22:32:23 GMT + +### Patches + +- Update documentation + +## 7.4.0 +Tue, 10 Sep 2019 20:38:33 GMT + +### Minor changes + +- Add 'canonicalReference' to ExcerptToken + +## 7.3.4 +Wed, 04 Sep 2019 18:28:06 GMT + +_Version update only_ + +## 7.3.3 +Wed, 04 Sep 2019 15:15:37 GMT + +### Patches + +- Update TSDoc dependency to 0.12.14 + +## 7.3.2 +Thu, 08 Aug 2019 15:14:17 GMT + +_Version update only_ + +## 7.3.1 +Thu, 08 Aug 2019 00:49:05 GMT + +### Patches + +- (Experimental) Add ApiExtractor.canonicalReference which is a beta implementation of the revised TSDoc declaration reference notation + +## 7.3.0 +Mon, 22 Jul 2019 19:13:10 GMT + +### Minor changes + +- Rename `ApiItem.canonicalReference` to `.containerKey`; rename `ApiItemContainerMixin.tryGetMember()` to `.tryGetMemberByKey()`; rename `Api___.getCanonicalReference()` to `.getContainerKey()` + +## 7.2.0 +Tue, 11 Jun 2019 00:48:06 GMT + +### Minor changes + +- Add API support for type parameters and type alias types + +### Patches + +- Improve the .api.json deserializer to validate the schema version and support backwards compatibility + +## 7.1.3 +Wed, 05 Jun 2019 19:12:34 GMT + +### Patches + +- Fix an issue where TSDoc index selectors (ApiParameterListMixin.overloadIndex) started from 0, whereas TSDoc requires a nonzero number + +## 7.1.2 +Tue, 04 Jun 2019 05:51:53 GMT + +### Patches + +- Fix an issue where ApiConstructor inherited from ApiStaticMixin, but TypeScript constructors cannot be static + +## 7.1.1 +Mon, 27 May 2019 04:13:44 GMT + +### Patches + +- Make the strings returned by ApiItem.displayName less verbose +- Improve formatting of the strings returned by ApiItem.getScopedNameWithinPackage() + +## 7.1.0 +Tue, 16 Apr 2019 11:01:37 GMT + +### Minor changes + +- Initial stable release of API Extractor 7 + +## 7.0.28 +Wed, 20 Mar 2019 19:14:49 GMT + +_Version update only_ + +## 7.0.27 +Mon, 18 Mar 2019 04:28:43 GMT + +### Patches + +- Add helper functions for ReleaseTag +- Export IApiItemConstructor to eliminate the ae-forgotten-export warning + +## 7.0.26 +Wed, 13 Mar 2019 19:13:14 GMT + +### Patches + +- Refactor code to move the IndentedWriter API from api-extractor-model to api-documenter + +## 7.0.25 +Wed, 13 Mar 2019 01:14:05 GMT + +### Patches + +- Upgrade TSDoc + +## 7.0.24 +Mon, 11 Mar 2019 16:13:36 GMT + +### Patches + +- Initial setup of new package @microsoft/api-extractor-model + diff --git a/packages/api-extractor-model/LICENSE b/packages/api-extractor-model/LICENSE new file mode 100644 index 000000000..d9cfa3eb0 --- /dev/null +++ b/packages/api-extractor-model/LICENSE @@ -0,0 +1,24 @@ +@microsoft/api-extractor + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/api-extractor-model/README.md b/packages/api-extractor-model/README.md new file mode 100644 index 000000000..8c5c794dc --- /dev/null +++ b/packages/api-extractor-model/README.md @@ -0,0 +1,67 @@ +# @microsoft/api-extractor-model + +Use this library to read and write \*.api.json files as defined by the [API Extractor](https://api-extractor.com/) tool. +These files are used to generate a documentation website for your TypeScript package. The files store the +API signatures and doc comments that were extracted from your package. + +API documentation for this package: https://rushstack.io/pages/api/api-extractor-model/ + +## Example Usage + +The following code sample shows how to load `example.api.json`, which would be generated by API Extractor +when it analyzes a hypothetical NPM package called `example`: + +```ts +import { ApiModel, ApiPackage } from '@discordjs/api-extractor-model'; + +const apiModel: ApiModel = new ApiModel(); +const apiPackage: ApiPackage = apiModel.loadPackage('example.api.json'); + +for (const member of apiPackage.members) { + console.log(member.displayName); +} +``` + +The `ApiModel` is acts as a container for various packages that are loaded and operated on as a group. +For example, a documentation tool may need to resolve `@link` references across different packages. +In this case we would load the various packages into the `ApiModel`, and then use +the `ApiModel.resolveDeclarationReference()` to resolve the `@link` targets. + +The data structure forms a tree of various classes that start with the `Api` prefix. The nesting hierarchy +might look like this: + +``` +- ApiModel + - ApiPackage + - ApiEntryPoint + - ApiClass + - ApiMethod + - ApiProperty + - ApiEnum + - ApiEnumMember + - ApiInterface + - ApiMethodSignature + - ApiPropertySignature + - ApiNamespace + - (ApiClass, ApiEnum, ApiInterface, ...) +``` + +You can use the `ApiItem.members` property to traverse this tree. + +Note that the non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use +TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various +features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class +to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: +the function that generates a subclass, an interface that describes the members of the subclass, and +a namespace containing static members of the class. + +> For a complete project that uses these APIs to generate an API reference web site, +> see the [@microsoft/api-documenter](https://www.npmjs.com/package/@microsoft/api-documenter) source code. + +## Links + +- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/libraries/api-extractor-model/CHANGELOG.md) - Find + out what's new in the latest version +- [API Reference](https://rushstack.io/pages/api/api-extractor-model/) + +API Extractor is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/packages/api-extractor-model/config/api-extractor.json b/packages/api-extractor-model/config/api-extractor.json new file mode 100644 index 000000000..c4d10dfdc --- /dev/null +++ b/packages/api-extractor-model/config/api-extractor.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "../../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/rollup.d.ts" + } +} diff --git a/packages/api-extractor-model/config/jest.config.json b/packages/api-extractor-model/config/jest.config.json new file mode 100644 index 000000000..9bd34de57 --- /dev/null +++ b/packages/api-extractor-model/config/jest.config.json @@ -0,0 +1,11 @@ +{ + "extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json", + + // Enable code coverage for Jest + "collectCoverage": true, + "coverageDirectory": "/coverage", + "coverageReporters": ["cobertura", "html"], + + // Use v8 coverage provider to avoid Babel + "coverageProvider": "v8" +} diff --git a/packages/api-extractor-model/package.json b/packages/api-extractor-model/package.json new file mode 100644 index 000000000..0c10afc60 --- /dev/null +++ b/packages/api-extractor-model/package.json @@ -0,0 +1,49 @@ +{ + "name": "@discordjs/api-extractor-model", + "version": "7.28.2", + "description": "A helper library for loading and saving the .api.json files created by API Extractor", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/discordjs/discord.js.git", + "directory": "packages/api-extractor-model" + }, + "homepage": "https://discord.js.org", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc --noEmit && tsup", + "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src", + "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src" + }, + "exports": { + ".": { + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2", + "@rushstack/node-core-library": "3.61.0" + }, + "devDependencies": { + "@types/node": "^18.18.8", + "@types/jest": "^29.5.7", + "cross-env": "^7.0.3", + "eslint": "^8.53.0", + "eslint-config-neon": "^0.1.57", + "eslint-formatter-pretty": "^5.0.0", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "tsup": "^7.2.0", + "turbo": "^1.10.16" + } +} diff --git a/packages/api-extractor-model/src/aedoc/AedocDefinitions.ts b/packages/api-extractor-model/src/aedoc/AedocDefinitions.ts new file mode 100644 index 000000000..b3c7d5895 --- /dev/null +++ b/packages/api-extractor-model/src/aedoc/AedocDefinitions.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { TSDocConfiguration, TSDocTagDefinition, TSDocTagSyntaxKind, StandardTags } from '@microsoft/tsdoc'; + +/** + * @internal + * @deprecated - tsdoc configuration is now constructed from tsdoc.json files associated with each package. + */ +export class AedocDefinitions { + public static readonly betaDocumentation: TSDocTagDefinition = new TSDocTagDefinition({ + tagName: '@betaDocumentation', + syntaxKind: TSDocTagSyntaxKind.ModifierTag, + }); + + public static readonly internalRemarks: TSDocTagDefinition = new TSDocTagDefinition({ + tagName: '@internalRemarks', + syntaxKind: TSDocTagSyntaxKind.BlockTag, + }); + + public static readonly preapprovedTag: TSDocTagDefinition = new TSDocTagDefinition({ + tagName: '@preapproved', + syntaxKind: TSDocTagSyntaxKind.ModifierTag, + }); + + public static get tsdocConfiguration(): TSDocConfiguration { + if (!AedocDefinitions._tsdocConfiguration) { + const configuration: TSDocConfiguration = new TSDocConfiguration(); + configuration.addTagDefinitions( + [AedocDefinitions.betaDocumentation, AedocDefinitions.internalRemarks, AedocDefinitions.preapprovedTag], + true, + ); + + configuration.setSupportForTags( + [ + StandardTags.alpha, + StandardTags.beta, + StandardTags.decorator, + StandardTags.defaultValue, + StandardTags.deprecated, + StandardTags.eventProperty, + StandardTags.example, + StandardTags.inheritDoc, + StandardTags.internal, + StandardTags.link, + StandardTags.override, + StandardTags.packageDocumentation, + StandardTags.param, + StandardTags.privateRemarks, + StandardTags.public, + StandardTags.readonly, + StandardTags.remarks, + StandardTags.returns, + StandardTags.sealed, + StandardTags.throws, + StandardTags.virtual, + ], + true, + ); + + AedocDefinitions._tsdocConfiguration = configuration; + } + + return AedocDefinitions._tsdocConfiguration; + } + + private static _tsdocConfiguration: TSDocConfiguration | undefined; +} diff --git a/packages/api-extractor-model/src/aedoc/ReleaseTag.ts b/packages/api-extractor-model/src/aedoc/ReleaseTag.ts new file mode 100644 index 000000000..c5fb1b5de --- /dev/null +++ b/packages/api-extractor-model/src/aedoc/ReleaseTag.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * A "release tag" is a custom TSDoc tag that is applied to an API to communicate the level of support + * provided for third-party developers. + * + * @remarks + * + * The four release tags are: `@internal`, `@alpha`, `@beta`, and `@public`. They are applied to API items such + * as classes, member functions, enums, etc. The release tag applies recursively to members of a container + * (e.g. class or interface). For example, if a class is marked as `@beta`, then all of its members automatically + * have this status; you DON'T need add the `@beta` tag to each member function. However, you could add + * `@internal` to a member function to give it a different release status. + * @public + */ +export enum ReleaseTag { + /** + * No release tag was specified in the AEDoc summary. + */ + None = 0, + /** + * Indicates that an API item is meant only for usage by other NPM packages from the same + * maintainer. Third parties should never use "internal" APIs. (To emphasize this, their + * names are prefixed by underscores.) + */ + Internal = 1, + /** + * Indicates that an API item is eventually intended to be public, but currently is in an + * early stage of development. Third parties should not use "alpha" APIs. + */ + Alpha = 2, + /** + * Indicates that an API item has been released in an experimental state. Third parties are + * encouraged to try it and provide feedback. However, a "beta" API should NOT be used + * in production. + */ + Beta = 3, + /** + * Indicates that an API item has been officially released. It is part of the supported + * contract (e.g. SemVer) for a package. + */ + Public = 4, +} + +/** + * Helper functions for working with the `ReleaseTag` enum. + * + * @public + */ + +// export namespace ReleaseTag { +/** + * Returns the TSDoc tag name for a `ReleaseTag` value. + * + * @remarks + * For example, `getTagName(ReleaseTag.Internal)` would return the string `@internal`. + */ +export function getTagName(releaseTag: ReleaseTag): string { + switch (releaseTag) { + case ReleaseTag.None: + return '(none)'; + case ReleaseTag.Internal: + return '@internal'; + case ReleaseTag.Alpha: + return '@alpha'; + case ReleaseTag.Beta: + return '@beta'; + case ReleaseTag.Public: + return '@public'; + default: + throw new Error('Unsupported release tag'); + } +} + +/** + * Compares two `ReleaseTag` values. Their values must not be `ReleaseTag.None`. + * + * @returns 0 if `a` and `b` are equal, a positive number if `a` is more public than `b`, + * and a negative number if `a` is less public than `b`. + * @remarks + * For example, `compareReleaseTag(ReleaseTag.Beta, ReleaseTag.Alpha)` will return a positive + * number because beta is more public than alpha. + */ +export function compare(a: ReleaseTag, b: ReleaseTag): number { + return a - b; +} +// } diff --git a/packages/api-extractor-model/src/index.ts b/packages/api-extractor-model/src/index.ts new file mode 100644 index 000000000..46905b8b0 --- /dev/null +++ b/packages/api-extractor-model/src/index.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Use this library to read and write *.api.json files as defined by the + * {@link https://api-extractor.com/ | API Extractor} tool. These files are used to generate a documentation + * website for your TypeScript package. The files store the API signatures and doc comments that were extracted + * from your package. + * + * @packageDocumentation + */ + +export { AedocDefinitions } from './aedoc/AedocDefinitions.js'; +export { ReleaseTag, compare as releaseTagCompare, getTagName as releaseTagGetTagName } from './aedoc/ReleaseTag.js'; + +// items +export { type IApiDeclaredItemOptions, ApiDeclaredItem } from './items/ApiDeclaredItem.js'; +export { type IApiDocumentedItemOptions, ApiDocumentedItem } from './items/ApiDocumentedItem.js'; +export { ApiItemKind, type IApiItemOptions, ApiItem, type IApiItemConstructor } from './items/ApiItem.js'; +export { type IApiPropertyItemOptions, ApiPropertyItem } from './items/ApiPropertyItem.js'; + +// mixins +export { + type IApiParameterListMixinOptions, + type IApiParameterOptions, + ApiParameterListMixin, +} from './mixins/ApiParameterListMixin.js'; +export { + type IApiTypeParameterOptions, + type IApiTypeParameterListMixinOptions, + ApiTypeParameterListMixin, +} from './mixins/ApiTypeParameterListMixin.js'; +export { type IApiAbstractMixinOptions, ApiAbstractMixin } from './mixins/ApiAbstractMixin.js'; +export { type IApiItemContainerMixinOptions, ApiItemContainerMixin } from './mixins/ApiItemContainerMixin.js'; +export { type IApiProtectedMixinOptions, ApiProtectedMixin } from './mixins/ApiProtectedMixin.js'; +export { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from './mixins/ApiReleaseTagMixin.js'; +export { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from './mixins/ApiReturnTypeMixin.js'; +export { type IApiStaticMixinOptions, ApiStaticMixin } from './mixins/ApiStaticMixin.js'; +export { type IApiNameMixinOptions, ApiNameMixin } from './mixins/ApiNameMixin.js'; +export { type IApiOptionalMixinOptions, ApiOptionalMixin } from './mixins/ApiOptionalMixin.js'; +export { type IApiReadonlyMixinOptions, ApiReadonlyMixin } from './mixins/ApiReadonlyMixin.js'; +export { type IApiInitializerMixinOptions, ApiInitializerMixin } from './mixins/ApiInitializerMixin.js'; +export { type IApiExportedMixinOptions, ApiExportedMixin } from './mixins/ApiExportedMixin.js'; +export { + type IFindApiItemsResult, + type IFindApiItemsMessage, + FindApiItemsMessageId, +} from './mixins/IFindApiItemsResult.js'; + +export { + ExcerptTokenKind, + type IExcerptTokenRange, + type IExcerptToken, + ExcerptToken, + Excerpt, +} from './mixins/Excerpt.js'; +export type { Constructor, PropertiesOf } from './mixins/Mixin.js'; + +// model +export { type IApiCallSignatureOptions, ApiCallSignature } from './model/ApiCallSignature.js'; +export { type IApiClassOptions, ApiClass } from './model/ApiClass.js'; +export { type IApiConstructorOptions, ApiConstructor } from './model/ApiConstructor.js'; +export { type IApiConstructSignatureOptions, ApiConstructSignature } from './model/ApiConstructSignature.js'; +export { type IApiEntryPointOptions, ApiEntryPoint } from './model/ApiEntryPoint.js'; +export { type IApiEnumOptions, ApiEnum } from './model/ApiEnum.js'; +export { type IApiEnumMemberOptions, ApiEnumMember, EnumMemberOrder } from './model/ApiEnumMember.js'; +export { type IApiFunctionOptions, ApiFunction } from './model/ApiFunction.js'; +export { type IApiIndexSignatureOptions, ApiIndexSignature } from './model/ApiIndexSignature.js'; +export { type IApiInterfaceOptions, ApiInterface } from './model/ApiInterface.js'; +export { type IApiMethodOptions, ApiMethod } from './model/ApiMethod.js'; +export { type IApiMethodSignatureOptions, ApiMethodSignature } from './model/ApiMethodSignature.js'; +export { ApiModel } from './model/ApiModel.js'; +export { type IApiNamespaceOptions, ApiNamespace } from './model/ApiNamespace.js'; +export { type IApiPackageOptions, ApiPackage, type IApiPackageSaveOptions } from './model/ApiPackage.js'; +export { type IParameterOptions, Parameter } from './model/Parameter.js'; +export { type IApiPropertyOptions, ApiProperty } from './model/ApiProperty.js'; +export { type IApiPropertySignatureOptions, ApiPropertySignature } from './model/ApiPropertySignature.js'; +export { type IApiTypeAliasOptions, ApiTypeAlias } from './model/ApiTypeAlias.js'; +export { type ITypeParameterOptions, TypeParameter } from './model/TypeParameter.js'; +export { type IApiVariableOptions, ApiVariable } from './model/ApiVariable.js'; +export type { IResolveDeclarationReferenceResult } from './model/ModelReferenceResolver.js'; +export { HeritageType } from './model/HeritageType.js'; +export { type ISourceLocationOptions, SourceLocation } from './model/SourceLocation.js'; +export { Navigation, Meaning } from './items/ApiItem.js'; diff --git a/packages/api-extractor-model/src/items/ApiDeclaredItem.ts b/packages/api-extractor-model/src/items/ApiDeclaredItem.ts new file mode 100644 index 000000000..f66b50e02 --- /dev/null +++ b/packages/api-extractor-model/src/items/ApiDeclaredItem.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { Excerpt, ExcerptToken, type IExcerptTokenRange, type IExcerptToken } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import { SourceLocation } from '../model/SourceLocation.js'; +import { ApiDocumentedItem, type IApiDocumentedItemJson, type IApiDocumentedItemOptions } from './ApiDocumentedItem.js'; +import type { ApiItem } from './ApiItem.js'; + +/** + * Constructor options for {@link ApiDeclaredItem}. + * + * @public + */ +export interface IApiDeclaredItemOptions extends IApiDocumentedItemOptions { + excerptTokens: IExcerptToken[]; + fileColumn?: number | undefined; + fileLine?: number | undefined; + fileUrlPath?: string | undefined; +} + +export interface IApiDeclaredItemJson extends IApiDocumentedItemJson { + excerptTokens: IExcerptToken[]; + fileColumn?: number; + fileLine?: number; + fileUrlPath?: string | undefined; +} + +/** + * The base class for API items that have an associated source code excerpt containing a TypeScript declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * Most `ApiItem` subclasses have declarations and thus extend `ApiDeclaredItem`. Counterexamples include + * `ApiModel` and `ApiPackage`, which do not have any corresponding TypeScript source code. + * @public + */ + +export class ApiDeclaredItem extends ApiDocumentedItem { + private readonly _excerptTokens: ExcerptToken[]; + + private readonly _excerpt: Excerpt; + + private readonly _fileUrlPath?: string | undefined; + + private readonly _fileLine?: number | undefined; + + private readonly _fileColumn?: number | undefined; + + private _sourceLocation?: SourceLocation; + + public constructor(options: IApiDeclaredItemOptions) { + super(options); + + this._excerptTokens = options.excerptTokens.map((token) => { + const canonicalReference: DeclarationReference | undefined = + token.canonicalReference === undefined ? undefined : DeclarationReference.parse(token.canonicalReference); + return new ExcerptToken(token.kind, token.text, canonicalReference); + }); + this._excerpt = new Excerpt(this.excerptTokens, { startIndex: 0, endIndex: this.excerptTokens.length }); + this._fileUrlPath = options.fileUrlPath; + this._fileLine = options.fileLine; + this._fileColumn = options.fileColumn; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiDeclaredItemJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.excerptTokens = jsonObject.excerptTokens; + options.fileUrlPath = jsonObject.fileUrlPath; + options.fileLine = jsonObject.fileLine; + options.fileColumn = jsonObject.fileColumn; + } + + /** + * The source code excerpt where the API item is declared. + */ + public get excerpt(): Excerpt { + return this._excerpt; + } + + /** + * The individual source code tokens that comprise the main excerpt. + */ + public get excerptTokens(): readonly ExcerptToken[] { + return this._excerptTokens; + } + + /** + * The file URL path relative to the `projectFolder` and `projectFolderURL` fields + * as defined in the `api-extractor.json` config. Is `undefined` if the path is + * the same as the parent API item's. + */ + public get fileUrlPath(): string | undefined { + return this._fileUrlPath; + } + + /** + * The line in the `fileUrlPath` where the API item is declared. + */ + public get fileLine(): number | undefined { + return this._fileLine; + } + + /** + * The column in the `fileUrlPath` where the API item is declared. + */ + public get fileColumn(): number | undefined { + return this._fileColumn; + } + + /** + * Returns the source location where the API item is declared. + */ + public get sourceLocation(): SourceLocation { + if (!this._sourceLocation) { + this._sourceLocation = this._buildSourceLocation(); + } + + return this._sourceLocation; + } + + /** + * If the API item has certain important modifier tags such as `@sealed`, `@virtual`, or `@override`, + * this prepends them as a doc comment above the excerpt. + */ + public getExcerptWithModifiers(): string { + const excerpt: string = this.excerpt.text; + const modifierTags: string[] = []; + + if (excerpt.length > 0 && this instanceof ApiDocumentedItem) { + if (this.tsdocComment) { + if (this.tsdocComment.modifierTagSet.isSealed()) { + modifierTags.push('@sealed'); + } + + if (this.tsdocComment.modifierTagSet.isVirtual()) { + modifierTags.push('@virtual'); + } + + if (this.tsdocComment.modifierTagSet.isOverride()) { + modifierTags.push('@override'); + } + } + + if (modifierTags.length > 0) { + return '/** ' + modifierTags.join(' ') + ' */\n' + excerpt; + } + } + + return excerpt; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + jsonObject.excerptTokens = this.excerptTokens.map((x) => { + const excerptToken: IExcerptToken = { kind: x.kind, text: x.text }; + if (x.canonicalReference !== undefined) { + excerptToken.canonicalReference = x.canonicalReference.toString(); + } + + return excerptToken; + }); + + // Only serialize this API item's file URL path if it exists and it's different from its parent's + // (a little optimization to keep the doc model succinct). + if ( + this.fileUrlPath && + (!(this.parent instanceof ApiDeclaredItem) || this.fileUrlPath !== this.parent.fileUrlPath) + ) { + jsonObject.fileUrlPath = this.fileUrlPath; + } + + if (this.fileLine) { + jsonObject.fileLine = this.fileLine; + } + + if (this.fileColumn) { + jsonObject.fileColumn = this.fileColumn; + } + } + + /** + * Constructs a new {@link Excerpt} corresponding to the provided token range. + */ + public buildExcerpt(tokenRange: IExcerptTokenRange): Excerpt { + return new Excerpt(this.excerptTokens, tokenRange); + } + + /** + * Builds the cached object used by the `sourceLocation` property. + */ + private _buildSourceLocation(): SourceLocation { + const projectFolderUrl: string | undefined = this.getAssociatedPackage()?.projectFolderUrl; + + let fileUrlPath: string | undefined; + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if (current instanceof ApiDeclaredItem && current.fileUrlPath) { + fileUrlPath = current.fileUrlPath; + break; + } + } + + return new SourceLocation({ + projectFolderUrl, + fileUrlPath, + sourceFileColumn: this.fileColumn, + sourceFileLine: this.fileLine, + }); + } +} diff --git a/packages/api-extractor-model/src/items/ApiDocumentedItem.ts b/packages/api-extractor-model/src/items/ApiDocumentedItem.ts new file mode 100644 index 000000000..777e49409 --- /dev/null +++ b/packages/api-extractor-model/src/items/ApiDocumentedItem.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as tsdoc from '@microsoft/tsdoc'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import { ApiItem, type IApiItemOptions, type IApiItemJson } from './ApiItem.js'; + +/** + * Constructor options for {@link ApiDocumentedItem}. + * + * @public + */ +export interface IApiDocumentedItemOptions extends IApiItemOptions { + docComment: tsdoc.DocComment | undefined; +} + +export interface IApiDocumentedItemJson extends IApiItemJson { + docComment: string; +} + +/** + * An abstract base class for API declarations that can have an associated TSDoc comment. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * @public + */ +export class ApiDocumentedItem extends ApiItem { + private readonly _tsdocComment: tsdoc.DocComment | undefined; + + public constructor(options: IApiDocumentedItemOptions) { + super(options); + this._tsdocComment = options.docComment; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiItemJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + const documentedJson: IApiDocumentedItemJson = jsonObject as IApiDocumentedItemJson; + + if (documentedJson.docComment) { + const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(context.tsdocConfiguration); + + // NOTE: For now, we ignore TSDoc errors found in a serialized .api.json file. + // Normally these errors would have already been reported by API Extractor during analysis. + // However, they could also arise if the JSON file was edited manually, or if the file was saved + // using a different release of the software that used an incompatible syntax. + const parserContext: tsdoc.ParserContext = tsdocParser.parseString(documentedJson.docComment); + + options.docComment = parserContext.docComment; + } + } + + public get tsdocComment(): tsdoc.DocComment | undefined { + return this._tsdocComment; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + if (this.tsdocComment === undefined) { + jsonObject.docComment = ''; + } else { + jsonObject.docComment = this.tsdocComment.emitAsTsdoc(); + } + } +} diff --git a/packages/api-extractor-model/src/items/ApiItem.ts b/packages/api-extractor-model/src/items/ApiItem.ts new file mode 100644 index 000000000..5cabaae12 --- /dev/null +++ b/packages/api-extractor-model/src/items/ApiItem.ts @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +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 { ApiModel } from '../model/ApiModel.js'; +import type { ApiPackage } from '../model/ApiPackage.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * The type returned by the {@link ApiItem.kind} property, which can be used to easily distinguish subclasses of + * {@link ApiItem}. + * + * @public + */ +export enum ApiItemKind { + CallSignature = 'CallSignature', + Class = 'Class', + ConstructSignature = 'ConstructSignature', + Constructor = 'Constructor', + EntryPoint = 'EntryPoint', + Enum = 'Enum', + EnumMember = 'EnumMember', + Function = 'Function', + IndexSignature = 'IndexSignature', + Interface = 'Interface', + Method = 'Method', + MethodSignature = 'MethodSignature', + Model = 'Model', + Namespace = 'Namespace', + None = 'None', + Package = 'Package', + Property = 'Property', + PropertySignature = 'PropertySignature', + TypeAlias = 'TypeAlias', + Variable = 'Variable', +} +/** + * Indicates the symbol table from which to resolve the next symbol component. + * + * @beta + */ +export enum Navigation { + Exports = '.', + Locals = '~', + Members = '#', +} +/** + * @beta + */ +export enum Meaning { + CallSignature = 'call', + Class = 'class', + ComplexType = 'complex', + ConstructSignature = 'new', + Constructor = 'constructor', + Enum = 'enum', + Event = 'event', + Function = 'function', + IndexSignature = 'index', + Interface = 'interface', + Member = 'member', + Namespace = 'namespace', + TypeAlias = 'type', + Variable = 'var', +} + +/** + * Constructor options for {@link ApiItem}. + * + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IApiItemOptions {} + +export interface IApiItemJson { + canonicalReference: string; + kind: ApiItemKind; +} + +// PRIVATE - Allows ApiItemContainerMixin to assign the parent. +// +export const apiItem_onParentChanged: unique symbol = Symbol('ApiItem._onAddToContainer'); + +/** + * The abstract base class for all members of an `ApiModel` object. + * + * @remarks + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * @public + */ +export class ApiItem { + private _canonicalReference: DeclarationReference | undefined; + + private _parent: ApiItem | undefined; + + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + public constructor(_options: IApiItemOptions) { + // ("options" is not used here, but part of the inheritance pattern) + } + + public static deserialize(jsonObject: IApiItemJson, context: DeserializerContext): ApiItem { + // The Deserializer class is coupled with a ton of other classes, so we delay loading it + // to avoid ES5 circular imports. + // eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const deserializerModule: typeof import('../model/Deserializer') = require('../model/Deserializer'); + return deserializerModule.Deserializer.deserialize(context, jsonObject); + } + + /** + * @virtual + */ + public static onDeserializeInto( + _options: Partial, + _context: DeserializerContext, + _jsonObject: IApiItemJson, + ): void { + // (implemented by subclasses) + } + + /** + * @virtual + */ + public serializeInto(jsonObject: Partial): void { + jsonObject.kind = this.kind; + jsonObject.canonicalReference = this.canonicalReference.toString(); + } + + /** + * Identifies the subclass of the `ApiItem` base class. + * + * @virtual + */ + public get kind(): ApiItemKind { + throw new Error('ApiItem.kind was not implemented by the child class'); + } + + /** + * Warning: This API is used internally by API extractor but is not yet ready for general usage. + * + * @remarks + * + * Returns a `DeclarationReference` object using the experimental new declaration reference notation. + * @beta + */ + public get canonicalReference(): DeclarationReference { + if (!this._canonicalReference) { + try { + this._canonicalReference = this.buildCanonicalReference(); + } catch (error) { + const name: string = this.getScopedNameWithinPackage() || this.displayName; + throw new InternalError(`Error building canonical reference for ${name}:\n` + (error as Error).message); + } + } + + return this._canonicalReference; + } + + /** + * Returns a string key that can be used to efficiently retrieve an `ApiItem` from an `ApiItemContainerMixin`. + * The key is unique within the container. Its format is undocumented and may change at any time. + * + * @remarks + * Use the `getContainerKey()` static member to construct the key. Each subclass has a different implementation + * of this function, according to the aspects that are important for identifying it. + * @virtual + */ + public get containerKey(): string { + throw new InternalError('ApiItem.containerKey was not implemented by the child class'); + } + + /** + * Returns a name for this object that can be used in diagnostic messages, for example. + * + * @remarks + * For an object that inherits ApiNameMixin, this will return the declared name (e.g. the name of a TypeScript + * function). Otherwise, it will return a string such as "(call signature)" or "(model)". + * @virtual + */ + public get displayName(): string { + switch (this.kind) { + case ApiItemKind.CallSignature: + return '(call)'; + case ApiItemKind.Constructor: + return '(constructor)'; + case ApiItemKind.ConstructSignature: + return '(new)'; + case ApiItemKind.IndexSignature: + return '(indexer)'; + case ApiItemKind.Model: + return '(model)'; + default: + return '(???)'; // All other types should inherit ApiNameMixin which will override this property + } + } + + /** + * If this item was added to a ApiItemContainerMixin item, then this returns the container item. + * If this is an Parameter that was added to a method or function, then this returns the function item. + * Otherwise, it returns undefined. + * + * @virtual + */ + public get parent(): ApiItem | undefined { + return this._parent; + } + + /** + * This property supports a visitor pattern for walking the tree. + * For items with ApiItemContainerMixin, it returns the contained items, sorted alphabetically. + * Otherwise it returns an empty array. + * + * @virtual + */ + public get members(): readonly ApiItem[] { + return []; + } + + /** + * If this item has a name (i.e. extends `ApiNameMixin`), then return all items that have the same parent + * and the same name. Otherwise, return all items that have the same parent and the same `ApiItemKind`. + * + * @remarks + * Examples: For a function, this would return all overloads for the function. For a constructor, this would + * return all overloads for the constructor. For a merged declaration (e.g. a `namespace` and `enum` with the + * same name), this would return both declarations. If this item does not have a parent, or if it is the only + * item of its name/kind, then the result is an array containing only this item. + */ + public getMergedSiblings(): readonly ApiItem[] { + const parent: ApiItem | undefined = this._parent; + if (parent && ApiItemContainerMixin.isBaseClassOf(parent)) { + return parent._getMergedSiblingsForMember(this); + } + + return []; + } + + /** + * Returns the chain of ancestors, starting from the root of the tree, and ending with the this item. + */ + public getHierarchy(): readonly ApiItem[] { + const hierarchy: ApiItem[] = []; + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + hierarchy.push(current); + } + + hierarchy.reverse(); + return hierarchy; + } + + /** + * This returns a scoped name such as `"Namespace1.Namespace2.MyClass.myMember()"`. It does not include the + * package name or entry point. + * + * @remarks + * If called on an ApiEntrypoint, ApiPackage, or ApiModel item, the result is an empty string. + */ + public getScopedNameWithinPackage(): string { + const reversedParts: string[] = []; + + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if ( + current.kind === ApiItemKind.Model || + current.kind === ApiItemKind.Package || + current.kind === ApiItemKind.EntryPoint + ) { + break; + } + + if (reversedParts.length === 0) { + switch (current.kind) { + case ApiItemKind.CallSignature: + case ApiItemKind.ConstructSignature: + case ApiItemKind.Constructor: + case ApiItemKind.IndexSignature: + // These functional forms don't have a proper name, so we don't append the "()" suffix + break; + default: + if (ApiParameterListMixin.isBaseClassOf(current)) { + reversedParts.push('()'); + } + } + } else { + reversedParts.push('.'); + } + + reversedParts.push(current.displayName); + } + + return reversedParts.reverse().join(''); + } + + /** + * If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned. + * Otherwise undefined is returned. + */ + public getAssociatedPackage(): ApiPackage | undefined { + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if (current.kind === ApiItemKind.Package) { + return current as ApiPackage; + } + } + + return undefined; + } + + /** + * If this item is an ApiModel or has an ApiModel as one of its parents, then that object is returned. + * Otherwise undefined is returned. + */ + public getAssociatedModel(): ApiModel | undefined { + for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) { + if (current.kind === ApiItemKind.Model) { + return current as ApiModel; + } + } + + return undefined; + } + + /** + * A text string whose value determines the sort order that is automatically applied by the + * {@link (ApiItemContainerMixin:interface)} class. + * + * @remarks + * The value of this string is undocumented and may change at any time. + * If {@link (ApiItemContainerMixin:interface).preserveMemberOrder} is enabled for the `ApiItem`'s parent, + * then no sorting is performed, and this key is not used. + * @virtual + */ + public getSortKey(): string { + return this.containerKey; + } + + /** + * PRIVATE + * + * @privateRemarks + * Allows ApiItemContainerMixin to assign the parent when the item is added to a container. + * @internal + */ + public [apiItem_onParentChanged](parent: ApiItem | undefined): void { + this._parent = parent; + this._canonicalReference = undefined; + } + + /** + * Builds the cached object used by the `canonicalReference` property. + * + * @virtual + */ + protected buildCanonicalReference(): DeclarationReference { + throw new InternalError('ApiItem.canonicalReference was not implemented by the child class'); + } +} + +/** + * This abstraction is used by the mixin pattern. + * It describes a class type that inherits from {@link ApiItem}. + * + * @public + */ +export interface IApiItemConstructor extends Constructor, PropertiesOf {} diff --git a/packages/api-extractor-model/src/items/ApiPropertyItem.ts b/packages/api-extractor-model/src/items/ApiPropertyItem.ts new file mode 100644 index 000000000..c6f6fafe8 --- /dev/null +++ b/packages/api-extractor-model/src/items/ApiPropertyItem.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js'; +import { ApiReadonlyMixin, type IApiReadonlyMixinOptions } from '../mixins/ApiReadonlyMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import type { Excerpt, IExcerptTokenRange } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem, type IApiDeclaredItemJson } from './ApiDeclaredItem.js'; + +/** + * Constructor options for {@link ApiPropertyItem}. + * + * @public + */ +export interface IApiPropertyItemOptions + extends IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiOptionalMixinOptions, + IApiReadonlyMixinOptions, + IApiDeclaredItemOptions { + propertyTypeTokenRange: IExcerptTokenRange; +} + +export interface IApiPropertyItemJson extends IApiDeclaredItemJson { + propertyTypeTokenRange: IExcerptTokenRange; +} + +/** + * The abstract base class for {@link ApiProperty} and {@link ApiPropertySignature}. + * + * @public + */ +export class ApiPropertyItem extends ApiNameMixin( + ApiReleaseTagMixin(ApiOptionalMixin(ApiReadonlyMixin(ApiDeclaredItem))), +) { + /** + * An {@link Excerpt} that describes the type of the property. + */ + public readonly propertyTypeExcerpt: Excerpt; + + public constructor(options: IApiPropertyItemOptions) { + super(options); + + this.propertyTypeExcerpt = this.buildExcerpt(options.propertyTypeTokenRange); + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiPropertyItemJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.propertyTypeTokenRange = jsonObject.propertyTypeTokenRange; + } + + /** + * Returns true if this property should be documented as an event. + * + * @remarks + * The `@eventProperty` TSDoc modifier can be added to readonly properties to indicate that they return an + * event object that event handlers can be attached to. The event-handling API is implementation-defined, but + * typically the return type would be a class with members such as `addHandler()` and `removeHandler()`. + * The documentation should display such properties under an "Events" heading instead of the + * usual "Properties" heading. + */ + public get isEventProperty(): boolean { + if (this.tsdocComment) { + return this.tsdocComment.modifierTagSet.isEventProperty(); + } + + return false; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.propertyTypeTokenRange = this.propertyTypeExcerpt.tokenRange; + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiAbstractMixin.ts b/packages/api-extractor-model/src/mixins/ApiAbstractMixin.ts new file mode 100644 index 000000000..301ce971a --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiAbstractMixin.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (ApiAbstractMixin:interface)}. + * + * @public + */ +export interface IApiAbstractMixinOptions extends IApiItemOptions { + isAbstract: boolean; +} + +export interface IApiAbstractMixinJson extends IApiItemJson { + isAbstract: boolean; +} + +const _isAbstract: unique symbol = Symbol('ApiAbstractMixin._isAbstract'); + +/** + * The mixin base class for API items that have an abstract modifier. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiAbstractMixin extends ApiItem { + /** + * Indicates that the API item's value has an 'abstract' modifier. + */ + readonly isAbstract: boolean; + + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiAbstractMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiAbstractMixin:interface)} + * functionality. + * @public + */ +export function ApiAbstractMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiAbstractMixin) { + class MixedClass extends baseClass implements ApiAbstractMixin { + public [_isAbstract]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiAbstractMixinOptions = args[0]; + this[_isAbstract] = options.isAbstract; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiAbstractMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.isAbstract = jsonObject.isAbstract || false; + } + + public get isAbstract(): boolean { + return this[_isAbstract]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isAbstract = this.isAbstract; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiAbstractMixin:interface)}. + * + * @public + */ +export namespace ApiAbstractMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiAbstractMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiAbstractMixin { + return apiItem.hasOwnProperty(_isAbstract); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiExportedMixin.ts b/packages/api-extractor-model/src/mixins/ApiExportedMixin.ts new file mode 100644 index 000000000..794691995 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiExportedMixin.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import { Navigation } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (IApiExportedMixinOptions:interface)}. + * + * @public + */ +export interface IApiExportedMixinOptions extends IApiItemOptions { + isExported: boolean; +} + +export interface IApiExportedMixinJson extends IApiItemJson { + isExported: boolean; +} + +const _isExported: unique symbol = Symbol('ApiExportedMixin._isExported'); + +/** + * The mixin base class for API items that can be exported. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiExportedMixin extends ApiItem { + /** + * Whether the declaration is exported from its parent item container (i.e. either an `ApiEntryPoint` or an + * `ApiNamespace`). + * + * @remarks + * Suppose `index.ts` is your entry point: + * + * ```ts + * // index.ts + * + * export class A {} + * class B {} + * + * namespace n { + * export class C {} + * class D {} + * } + * + * // file.ts + * export class E {} + * ``` + * + * Classes `A` and `C` are both exported, while classes `B`, `D`, and `E` are not. `E` is exported from its + * local file, but not from its parent item container (i.e. the entry point). + */ + readonly isExported: boolean; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiExportedMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiExportedMixin:interface)} functionality. + * @public + */ +export function ApiExportedMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiExportedMixin) { + class MixedClass extends baseClass implements ApiExportedMixin { + public [_isExported]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiExportedMixinOptions = args[0]; + this[_isExported] = options.isExported; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiExportedMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + const declarationReference: DeclarationReference = DeclarationReference.parse(jsonObject.canonicalReference); + options.isExported = declarationReference.navigation === (Navigation.Exports as any); // ambient const enums suck... + } + + public get isExported(): boolean { + return this[_isExported]; + } + + /** + * The `isExported` property is intentionally not serialized because the information is already present + * in the item's `canonicalReference`. + * + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiExportedMixin:interface)}. + * + * @public + */ +export namespace ApiExportedMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiExportedMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiExportedMixin { + return apiItem.hasOwnProperty(_isExported); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiInitializerMixin.ts b/packages/api-extractor-model/src/mixins/ApiInitializerMixin.ts new file mode 100644 index 000000000..e54f93f8f --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiInitializerMixin.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import type { IExcerptTokenRange, Excerpt } from './Excerpt.js'; + +/** + * Constructor options for {@link (IApiInitializerMixinOptions:interface)}. + * + * @public + */ +export interface IApiInitializerMixinOptions extends IApiItemOptions { + initializerTokenRange?: IExcerptTokenRange | undefined; +} + +export interface IApiInitializerMixinJson extends IApiItemJson { + initializerTokenRange?: IExcerptTokenRange; +} + +const _initializerExcerpt: unique symbol = Symbol('ApiInitializerMixin._initializerExcerpt'); + +/** + * The mixin base class for API items that can have an initializer. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiInitializerMixin extends ApiItem { + /** + * An {@link Excerpt} that describes the item's initializer. + */ + readonly initializerExcerpt?: Excerpt | undefined; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiInitializerMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiInitializerMixin:interface)} functionality. + * @public + */ +export function ApiInitializerMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiInitializerMixin) { + class MixedClass extends baseClass implements ApiInitializerMixin { + public [_initializerExcerpt]?: Excerpt; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiInitializerMixinOptions = args[0]; + + if (this instanceof ApiDeclaredItem) { + if (options.initializerTokenRange) { + this[_initializerExcerpt] = this.buildExcerpt(options.initializerTokenRange); + } + } else { + throw new InternalError('ApiInitializerMixin expects a base class that inherits from ApiDeclaredItem'); + } + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiInitializerMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.initializerTokenRange = jsonObject.initializerTokenRange; + } + + public get initializerExcerpt(): Excerpt | undefined { + return this[_initializerExcerpt]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + // Note that JSON does not support the "undefined" value, so we simply omit the field entirely if it is undefined + if (this.initializerExcerpt) { + jsonObject.initializerTokenRange = this.initializerExcerpt.tokenRange; + } + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiInitializerMixin:interface)}. + * + * @public + */ +export namespace ApiInitializerMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiInitializerMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiInitializerMixin { + return apiItem.hasOwnProperty(_initializerExcerpt); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts new file mode 100644 index 000000000..5959e03cf --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts @@ -0,0 +1,562 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { InternalError } from '@rushstack/node-core-library'; +import { + ApiItem, + apiItem_onParentChanged, + type IApiItemJson, + type IApiItemOptions, + type IApiItemConstructor, + ApiItemKind, +} from '../items/ApiItem.js'; +import type { ApiClass } from '../model/ApiClass.js'; +import type { ApiInterface } from '../model/ApiInterface.js'; +import type { ApiModel } from '../model/ApiModel.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import type { HeritageType } from '../model/HeritageType.js'; +import type { IResolveDeclarationReferenceResult } from '../model/ModelReferenceResolver.js'; +import { ApiNameMixin } from './ApiNameMixin.js'; +import { type ExcerptToken, ExcerptTokenKind } from './Excerpt.js'; +import { type IFindApiItemsResult, type IFindApiItemsMessage, FindApiItemsMessageId } from './IFindApiItemsResult.js'; + +/** + * Constructor options for {@link (ApiItemContainerMixin:interface)}. + * + * @public + */ +export interface IApiItemContainerMixinOptions extends IApiItemOptions { + members?: ApiItem[] | undefined; + preserveMemberOrder?: boolean | undefined; +} + +export interface IApiItemContainerJson extends IApiItemJson { + members: IApiItemJson[]; + preserveMemberOrder?: boolean; +} + +const _members: unique symbol = Symbol('ApiItemContainerMixin._members'); +const _membersSorted: unique symbol = Symbol('ApiItemContainerMixin._membersSorted'); +const _membersByContainerKey: unique symbol = Symbol('ApiItemContainerMixin._membersByContainerKey'); +const _membersByName: unique symbol = Symbol('ApiItemContainerMixin._membersByName'); +const _membersByKind: unique symbol = Symbol('ApiItemContainerMixin._membersByKind'); +const _preserveMemberOrder: unique symbol = Symbol('ApiItemContainerMixin._preserveMemberOrder'); + +/** + * The mixin base class for API items that act as containers for other child items. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * + * Examples of `ApiItemContainerMixin` child classes include `ApiModel`, `ApiPackage`, `ApiEntryPoint`, + * and `ApiEnum`. But note that `Parameter` is not considered a "member" of an `ApiMethod`; this relationship + * is modeled using {@link (ApiParameterListMixin:interface).parameters} instead + * of {@link ApiItem.members}. + * @public + */ + +export interface ApiItemContainerMixin extends ApiItem { + /** + * For a given member of this container, return its `ApiItem.getMergedSiblings()` list. + * + * @internal + */ + _getMergedSiblingsForMember(memberApiItem: ApiItem): readonly ApiItem[]; + + /** + * Adds a new member to the container. + * + * @remarks + * An ApiItem cannot be added to more than one container. + */ + addMember(member: ApiItem): void; + + /** + * Returns a list of members with the specified name. + */ + findMembersByName(name: string): readonly ApiItem[]; + + /** + * Finds all of the ApiItem's immediate and inherited members by walking up the inheritance tree. + * + * @remarks + * + * Given the following class heritage: + * + * ``` + * export class A { + * public a: number|boolean; + * } + * + * export class B extends A { + * public a: number; + * public b: string; + * } + * + * export class C extends B { + * public c: boolean; + * } + * ``` + * + * Calling `findMembersWithInheritance` on `C` will return `B.a`, `B.b`, and `C.c`. Calling the + * method on `B` will return `B.a` and `B.b`. And calling the method on `A` will return just + * `A.a`. + * + * The inherited members returned by this method may be incomplete. If so, there will be a flag + * on the result object indicating this as well as messages explaining the errors in more detail. + * Some scenarios include: + * + * - Interface extending from a type alias. + * + * - Class extending from a variable. + * + * - Extending from a declaration not present in the model (e.g. external package). + * + * - Extending from an unexported declaration (e.g. ae-forgotten-export). Common in mixin + * patterns. + * + * - Unexpected runtime errors... + * + * Lastly, be aware that the types of inherited members are returned with respect to their + * defining class as opposed to with respect to the inheriting class. For example, consider + * the following: + * + * ``` + * export class A { + * public a: T; + * } + * + * export class B extends A {} + * ``` + * + * When called on `B`, this method will return `B.a` with type `T` as opposed to type + * `number`, although the latter is more accurate. + */ + findMembersWithInheritance(): IFindApiItemsResult; + + /** + * Disables automatic sorting of {@link ApiItem.members}. + * + * @remarks + * By default `ApiItemContainerMixin` will automatically sort its members according to their + * {@link ApiItem.getSortKey} string, which provides a standardized mostly alphabetical ordering + * that is appropriate for most API items. When loading older .api.json files the automatic sorting + * is reapplied and may update the ordering. + * + * Set `preserveMemberOrder` to true to disable automatic sorting for this container; instead, the + * members will retain whatever ordering appeared in the {@link IApiItemContainerMixinOptions.members} array. + * The `preserveMemberOrder` option is saved in the .api.json file. + */ + readonly preserveMemberOrder: boolean; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; + + /** + * Attempts to retrieve a member using its containerKey, or returns `undefined` if no matching member was found. + * + * @remarks + * Use the `getContainerKey()` static member to construct the key. Each subclass has a different implementation + * of this function, according to the aspects that are important for identifying it. + * + * See {@link ApiItem.containerKey} for more information. + */ + tryGetMemberByKey(containerKey: string): ApiItem | undefined; +} + +/** + * Mixin function for {@link ApiDeclaredItem}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiItemContainerMixin:interface)} functionality. + * @public + */ +export function ApiItemContainerMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiItemContainerMixin) { + class MixedClass extends baseClass implements ApiItemContainerMixin { + public readonly [_members]: ApiItem[]; + + public [_membersSorted]: boolean; + + public [_membersByContainerKey]: Map; + + public [_preserveMemberOrder]: boolean; + + // For members of this container that extend ApiNameMixin, this stores the list of members with a given name. + // Examples include merged declarations, overloaded functions, etc. + public [_membersByName]: Map | undefined; + + // For members of this container that do NOT extend ApiNameMixin, this stores the list of members + // that share a common ApiItemKind. Examples include overloaded constructors or index signatures. + public [_membersByKind]: Map | undefined; // key is ApiItemKind + + public constructor(...args: any[]) { + super(...args); + const options: IApiItemContainerMixinOptions = args[0] as IApiItemContainerMixinOptions; + + this[_members] = []; + this[_membersSorted] = false; + this[_membersByContainerKey] = new Map(); + this[_preserveMemberOrder] = options.preserveMemberOrder ?? false; + + if (options.members) { + for (const member of options.members) { + this.addMember(member); + } + } + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiItemContainerJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + options.preserveMemberOrder = jsonObject.preserveMemberOrder; + options.members = []; + for (const memberObject of jsonObject.members) { + options.members.push(ApiItem.deserialize(memberObject, context)); + } + } + + /** + * @override + */ + public override get members(): readonly ApiItem[] { + if (!this[_membersSorted] && !this[_preserveMemberOrder]) { + this[_members].sort((x, y) => x.getSortKey().localeCompare(y.getSortKey())); + this[_membersSorted] = true; + } + + return this[_members]; + } + + public get preserveMemberOrder(): boolean { + return this[_preserveMemberOrder]; + } + + public addMember(member: ApiItem): void { + if (this[_membersByContainerKey].has(member.containerKey)) { + throw new Error( + `Another member has already been added with the same name (${member.displayName})` + + ` and containerKey (${member.containerKey})`, + ); + } + + const existingParent: ApiItem | undefined = member.parent; + if (existingParent !== undefined) { + throw new Error(`This item has already been added to another container: "${existingParent.displayName}"`); + } + + this[_members].push(member); + this[_membersByName] = undefined; // invalidate the lookup + this[_membersByKind] = undefined; // invalidate the lookup + this[_membersSorted] = false; + this[_membersByContainerKey].set(member.containerKey, member); + + member[apiItem_onParentChanged](this); + } + + public tryGetMemberByKey(containerKey: string): ApiItem | undefined { + return this[_membersByContainerKey].get(containerKey); + } + + public findMembersByName(name: string): readonly ApiItem[] { + this._ensureMemberMaps(); + return this[_membersByName]!.get(name) ?? []; + } + + public findMembersWithInheritance(): IFindApiItemsResult { + const messages: IFindApiItemsMessage[] = []; + let maybeIncompleteResult = false; + + // For API items that don't support inheritance, this method just returns the item's + // immediate members. + switch (this.kind) { + case ApiItemKind.Class: + case ApiItemKind.Interface: + break; + default: { + return { + items: this.members.concat(), + messages, + maybeIncompleteResult, + }; + } + } + + const membersByName: Map = new Map(); + const membersByKind: Map = new Map(); + + const toVisit: ApiItem[] = []; + let next: ApiItem | undefined = this; + + while (next) { + const membersToAdd: ApiItem[] = []; + + // For each member, check to see if we've already seen a member with the same name + // previously in the inheritance tree. If so, we know we won't inherit it, and thus + // do not add it to our `membersToAdd` array. + for (const member of next.members) { + // We add the to-be-added members to an intermediate array instead of immediately + // to the maps themselves to support method overloads with the same name. + if (ApiNameMixin.isBaseClassOf(member)) { + if (!membersByName.has(member.name)) { + membersToAdd.push(member); + } + } else if (!membersByKind.has(member.kind)) { + membersToAdd.push(member); + } + } + + for (const member of membersToAdd) { + if (ApiNameMixin.isBaseClassOf(member)) { + const members: ApiItem[] = membersByName.get(member.name) ?? []; + members.push(member); + membersByName.set(member.name, members); + } else { + const members: ApiItem[] = membersByKind.get(member.kind) ?? []; + members.push(member); + membersByKind.set(member.kind, members); + } + } + + // Interfaces can extend multiple interfaces, so iterate through all of them. + const extendedItems: ApiItem[] = []; + let extendsTypes: readonly HeritageType[] | undefined; + + switch (next.kind) { + case ApiItemKind.Class: { + const apiClass: ApiClass = next as ApiClass; + extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : []; + break; + } + + case ApiItemKind.Interface: { + const apiInterface: ApiInterface = next as ApiInterface; + extendsTypes = apiInterface.extendsTypes; + break; + } + + default: + break; + } + + if (extendsTypes === undefined) { + messages.push({ + messageId: FindApiItemsMessageId.UnsupportedKind, + text: `Unable to analyze references of API item ${next.displayName} because it is of unsupported kind ${next.kind}`, + }); + maybeIncompleteResult = true; + next = toVisit.shift(); + continue; + } + + for (const extendsType of extendsTypes) { + // We want to find the reference token associated with the actual inherited declaration. + // In every case we support, this is the first reference token. For example: + // + // ``` + // export class A extends B {} + // ^ + // export class A extends B {} + // ^ + // export class A extends B.C {} + // ^^^ + // ``` + const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find( + (token: ExcerptToken) => { + return token.kind === ExcerptTokenKind.Reference && token.canonicalReference; + }, + ); + + if (!firstReferenceToken) { + messages.push({ + messageId: FindApiItemsMessageId.ExtendsClauseMissingReference, + text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.displayName} because no canonical reference was found`, + }); + maybeIncompleteResult = true; + continue; + } + + const apiModel: ApiModel | undefined = this.getAssociatedModel(); + if (!apiModel) { + messages.push({ + messageId: FindApiItemsMessageId.NoAssociatedApiModel, + text: `Unable to analyze references of API item ${next.displayName} because it is not associated with an ApiModel`, + }); + maybeIncompleteResult = true; + continue; + } + + const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!; + const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference( + canonicalReference, + undefined, + ); + + const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem; + if (!apiItem) { + messages.push({ + messageId: FindApiItemsMessageId.DeclarationResolutionFailed, + text: `Unable to resolve declaration reference within API item ${next.displayName}: ${apiItemResult.errorMessage}`, + }); + maybeIncompleteResult = true; + continue; + } + + extendedItems.push(apiItem); + } + + // For classes, this array will only have one item. For interfaces, there may be multiple items. Sort the array + // into alphabetical order before adding to our list of API items to visit. This ensures that in the case + // of multiple interface inheritance, a member inherited from multiple interfaces is attributed to the interface + // earlier in alphabetical order (as opposed to source order). + // + // For example, in the code block below, `Bar.x` is reported as the inherited item, not `Foo.x`. + // + // ``` + // interface Foo { + // public x: string; + // } + // + // interface Bar { + // public x: string; + // } + // + // interface FooBar extends Foo, Bar {} + // ``` + extendedItems.sort((x: ApiItem, y: ApiItem) => x.getSortKey().localeCompare(y.getSortKey())); + + toVisit.push(...extendedItems); + next = toVisit.shift(); + } + + const items: ApiItem[] = []; + for (const members of membersByName.values()) { + items.push(...members); + } + + for (const members of membersByKind.values()) { + items.push(...members); + } + + items.sort((x: ApiItem, y: ApiItem) => x.getSortKey().localeCompare(y.getSortKey())); + + return { + items, + messages, + maybeIncompleteResult, + }; + } + + /** + * @internal + */ + public _getMergedSiblingsForMember(memberApiItem: ApiItem): readonly ApiItem[] { + this._ensureMemberMaps(); + let result: ApiItem[] | undefined; + if (ApiNameMixin.isBaseClassOf(memberApiItem)) { + result = this[_membersByName]!.get(memberApiItem.name); + } else { + result = this[_membersByKind]!.get(memberApiItem.kind); + } + + if (!result) { + throw new InternalError('Item was not found in the _membersByName/_membersByKind lookup'); + } + + return result; + } + + /** + * @internal + */ + public _ensureMemberMaps(): void { + // Build the _membersByName and _membersByKind tables if they don't already exist + if (this[_membersByName] === undefined) { + const membersByName: Map = new Map(); + const membersByKind: Map = new Map(); + + for (const member of this[_members]) { + let map: Map | Map; + let key: ApiItemKind | string; + + if (ApiNameMixin.isBaseClassOf(member)) { + map = membersByName; + key = member.name; + } else { + map = membersByKind; + key = member.kind; + } + + let list: ApiItem[] | undefined = map.get(key); + if (list === undefined) { + list = []; + map.set(key, list); + } + + list.push(member); + } + + this[_membersByName] = membersByName; + this[_membersByKind] = membersByKind; + } + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + const memberObjects: IApiItemJson[] = []; + + for (const member of this.members) { + const memberJsonObject: Partial = {}; + member.serializeInto(memberJsonObject); + memberObjects.push(memberJsonObject as IApiItemJson); + } + + jsonObject.preserveMemberOrder = this.preserveMemberOrder; + jsonObject.members = memberObjects; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiItemContainerMixin:interface)}. + * + * @public + */ + +export namespace ApiItemContainerMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiItemContainerMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiItemContainerMixin { + return apiItem.hasOwnProperty(_members); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiNameMixin.ts b/packages/api-extractor-model/src/mixins/ApiNameMixin.ts new file mode 100644 index 000000000..c229aa874 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiNameMixin.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (IApiNameMixinOptions:interface)}. + * + * @public + */ +export interface IApiNameMixinOptions extends IApiItemOptions { + name: string; +} + +export interface IApiNameMixinJson extends IApiItemJson { + name: string; +} + +const _name: unique symbol = Symbol('ApiNameMixin._name'); + +/** + * The mixin base class for API items that have a name. For example, a class has a name, but a class constructor + * does not. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiNameMixin extends ApiItem { + /** + * The exported name of this API item. + * + * @remarks + * Note that due tue type aliasing, the exported name may be different from the locally declared name. + */ + readonly name: string; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiNameMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiNameMixin:interface)} functionality. + * @public + */ +export function ApiNameMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiNameMixin) { + class MixedClass extends baseClass implements ApiNameMixin { + public readonly [_name]: string; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiNameMixinOptions = args[0]; + this[_name] = options.name; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiNameMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.name = jsonObject.name; + } + + public get name(): string { + return this[_name]; + } + + /** + * @override + */ + public override get displayName(): string { + return this[_name]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.name = this.name; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiNameMixin:interface)}. + * + * @public + */ +export namespace ApiNameMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiNameMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiNameMixin { + return apiItem.hasOwnProperty(_name); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiOptionalMixin.ts b/packages/api-extractor-model/src/mixins/ApiOptionalMixin.ts new file mode 100644 index 000000000..5abff87cb --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiOptionalMixin.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (IApiOptionalMixinOptions:interface)}. + * + * @public + */ +export interface IApiOptionalMixinOptions extends IApiItemOptions { + isOptional: boolean; +} + +export interface IApiOptionalMixinJson extends IApiItemJson { + isOptional: boolean; +} + +const _isOptional: unique symbol = Symbol('ApiOptionalMixin._isOptional'); + +/** + * The mixin base class for API items that can be marked as optional by appending a `?` to them. + * For example, a property of an interface can be optional. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiOptionalMixin extends ApiItem { + /** + * True if this is an optional property. + * + * @remarks + * For example: + * ```ts + * interface X { + * y: string; // not optional + * z?: string; // optional + * } + * ``` + */ + readonly isOptional: boolean; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiOptionalMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiOptionalMixin:interface)} functionality. + * @public + */ +export function ApiOptionalMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiOptionalMixin) { + class MixedClass extends baseClass implements ApiOptionalMixin { + public [_isOptional]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiOptionalMixinOptions = args[0]; + this[_isOptional] = Boolean(options.isOptional); + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiOptionalMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.isOptional = Boolean(jsonObject.isOptional); + } + + public get isOptional(): boolean { + return this[_isOptional]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isOptional = this.isOptional; + } + } + + return MixedClass; +} + +/** + * Optional members for {@link (ApiOptionalMixin:interface)}. + * + * @public + */ +export namespace ApiOptionalMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiOptionalMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiOptionalMixin { + return apiItem.hasOwnProperty(_isOptional); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiParameterListMixin.ts b/packages/api-extractor-model/src/mixins/ApiParameterListMixin.ts new file mode 100644 index 000000000..5df2f2d49 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiParameterListMixin.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import { Parameter } from '../model/Parameter.js'; +import type { IExcerptTokenRange } from './Excerpt.js'; + +/** + * Represents parameter information that is part of {@link IApiParameterListMixinOptions} + * + * @public + */ +export interface IApiParameterOptions { + isOptional: boolean; + isRest: boolean; + parameterName: string; + parameterTypeTokenRange: IExcerptTokenRange; +} + +/** + * Constructor options for {@link (ApiParameterListMixin:interface)}. + * + * @public + */ +export interface IApiParameterListMixinOptions extends IApiItemOptions { + overloadIndex: number; + parameters: IApiParameterOptions[]; +} + +export interface IApiParameterListJson extends IApiItemJson { + overloadIndex: number; + parameters: IApiParameterOptions[]; +} + +const _overloadIndex: unique symbol = Symbol('ApiParameterListMixin._overloadIndex'); +const _parameters: unique symbol = Symbol('ApiParameterListMixin._parameters'); + +/** + * The mixin base class for API items that can have function parameters (but not necessarily a return value). + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiParameterListMixin extends ApiItem { + /** + * When a function has multiple overloaded declarations, this one-based integer index can be used to uniquely + * identify them. + * + * @remarks + * + * Consider this overloaded declaration: + * + * ```ts + * export namespace Versioning { + * // TSDoc: Versioning.(addVersions:1) + * export function addVersions(x: number, y: number): number; + * + * // TSDoc: Versioning.(addVersions:2) + * export function addVersions(x: string, y: string): string; + * + * // (implementation) + * export function addVersions(x: number|string, y: number|string): number|string { + * // . . . + * } + * } + * ``` + * + * In the above example, there are two overloaded declarations. The overload using numbers will have + * `overloadIndex = 1`. The overload using strings will have `overloadIndex = 2`. The third declaration that + * accepts all possible inputs is considered part of the implementation, and is not processed by API Extractor. + */ + readonly overloadIndex: number; + + /** + * The function parameters. + */ + readonly parameters: readonly Parameter[]; + + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiParameterListMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiParameterListMixin:interface)} functionality. + * @public + */ +export function ApiParameterListMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiParameterListMixin) { + class MixedClass extends baseClass implements ApiParameterListMixin { + public readonly [_overloadIndex]: number; + + public readonly [_parameters]: Parameter[]; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiParameterListMixinOptions = args[0]; + this[_overloadIndex] = options.overloadIndex; + + this[_parameters] = []; + + if (this instanceof ApiDeclaredItem) { + if (options.parameters) { + for (const parameterOptions of options.parameters) { + const parameter: Parameter = new Parameter({ + name: parameterOptions.parameterName, + parameterTypeExcerpt: this.buildExcerpt(parameterOptions.parameterTypeTokenRange), + // Prior to ApiJsonSchemaVersion.V_1005 this input will be undefined + isOptional: Boolean(parameterOptions.isOptional), + isRest: Boolean(parameterOptions.isRest), + parent: this, + }); + + this[_parameters].push(parameter); + } + } + } else { + throw new InternalError('ApiReturnTypeMixin expects a base class that inherits from ApiDeclaredItem'); + } + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiParameterListJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.overloadIndex = jsonObject.overloadIndex; + options.parameters = jsonObject.parameters || []; + } + + public get overloadIndex(): number { + return this[_overloadIndex]; + } + + public get parameters(): readonly Parameter[] { + return this[_parameters]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.overloadIndex = this.overloadIndex; + + const parameterObjects: IApiParameterOptions[] = []; + for (const parameter of this.parameters) { + parameterObjects.push({ + parameterName: parameter.name, + parameterTypeTokenRange: parameter.parameterTypeExcerpt.tokenRange, + isOptional: parameter.isOptional, + isRest: parameter.isRest, + }); + } + + jsonObject.parameters = parameterObjects; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiParameterListMixin:interface)}. + * + * @public + */ +export namespace ApiParameterListMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiParameterListMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiParameterListMixin { + return apiItem.hasOwnProperty(_parameters); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiProtectedMixin.ts b/packages/api-extractor-model/src/mixins/ApiProtectedMixin.ts new file mode 100644 index 000000000..08be78775 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiProtectedMixin.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (IApiProtectedMixinOptions:interface)}. + * + * @public + */ +export interface IApiProtectedMixinOptions extends IApiItemOptions { + isProtected: boolean; +} + +export interface IApiProtectedMixinJson extends IApiItemJson { + isProtected: boolean; +} + +const _isProtected: unique symbol = Symbol('ApiProtectedMixin._isProtected'); + +/** + * The mixin base class for API items that can have the TypeScript `protected` keyword applied to them. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiProtectedMixin extends ApiItem { + /** + * Whether the declaration has the TypeScript `protected` keyword. + */ + readonly isProtected: boolean; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiProtectedMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiProtectedMixin:interface)} functionality. + * @public + */ +export function ApiProtectedMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiProtectedMixin) { + class MixedClass extends baseClass implements ApiProtectedMixin { + public [_isProtected]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiProtectedMixinOptions = args[0]; + this[_isProtected] = options.isProtected; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiProtectedMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.isProtected = jsonObject.isProtected; + } + + public get isProtected(): boolean { + return this[_isProtected]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isProtected = this.isProtected; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiProtectedMixin:interface)}. + * + * @public + */ +export namespace ApiProtectedMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiProtectedMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiProtectedMixin { + return apiItem.hasOwnProperty(_isProtected); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiReadonlyMixin.ts b/packages/api-extractor-model/src/mixins/ApiReadonlyMixin.ts new file mode 100644 index 000000000..08584bc13 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiReadonlyMixin.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (ApiReadonlyMixin:interface)}. + * + * @public + */ +export interface IApiReadonlyMixinOptions extends IApiItemOptions { + isReadonly: boolean; +} + +export interface IApiReadonlyMixinJson extends IApiItemJson { + isReadonly: boolean; +} + +const _isReadonly: unique symbol = Symbol('ApiReadonlyMixin._isReadonly'); + +/** + * The mixin base class for API items that cannot be modified after instantiation. + * Examples such as the readonly modifier and only having a getter but no setter. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiReadonlyMixin extends ApiItem { + /** + * Indicates that the API item's value cannot be assigned by an external consumer. + * + * @remarks + * Examples of API items that would be considered "read only" by API Extractor: + * + * - A class or interface's property that has the `readonly` modifier. + * + * - A variable that has the `const` modifier. + * + * - A property or variable whose TSDoc comment includes the `@readonly` tag. + * + * - A property declaration with a getter but no setter. + * + * Note that if the `readonly` keyword appears in a type annotation, this does not + * guarantee that that the API item will be considered readonly. For example: + * + * ```ts + * declare class C { + * // isReadonly=false in this case, because C.x is assignable + * public x: readonly string[]; + * } + * ``` + */ + readonly isReadonly: boolean; + + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiReadonlyMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiReadonlyMixin:interface)} + * functionality. + * @public + */ +export function ApiReadonlyMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiReadonlyMixin) { + class MixedClass extends baseClass implements ApiReadonlyMixin { + public [_isReadonly]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiReadonlyMixinOptions = args[0]; + this[_isReadonly] = options.isReadonly; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiReadonlyMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.isReadonly = jsonObject.isReadonly || false; + } + + public get isReadonly(): boolean { + return this[_isReadonly]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isReadonly = this.isReadonly; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiReadonlyMixin:interface)}. + * + * @public + */ +export namespace ApiReadonlyMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReadonlyMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReadonlyMixin { + return apiItem.hasOwnProperty(_isReadonly); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiReleaseTagMixin.ts b/packages/api-extractor-model/src/mixins/ApiReleaseTagMixin.ts new file mode 100644 index 000000000..13f6ff0a3 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiReleaseTagMixin.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Enum } from '@rushstack/node-core-library'; +import { ReleaseTag } from '../aedoc/ReleaseTag.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (ApiReleaseTagMixin:interface)}. + * + * @public + */ +export interface IApiReleaseTagMixinOptions extends IApiItemOptions { + releaseTag: ReleaseTag; +} + +export interface IApiReleaseTagMixinJson extends IApiItemJson { + releaseTag: string; +} + +const _releaseTag: unique symbol = Symbol('ApiReleaseTagMixin._releaseTag'); + +/** + * The mixin base class for API items that can be attributed with a TSDoc tag such as `@internal`, + * `@alpha`, `@beta`, or `@public`. These "release tags" indicate the support level for an API. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiReleaseTagMixin extends ApiItem { + /** + * The effective release tag for this declaration. If it is not explicitly specified, the value may be + * inherited from a containing declaration. + * + * @remarks + * For example, an `ApiEnumMember` may inherit its release tag from the containing `ApiEnum`. + */ + readonly releaseTag: ReleaseTag; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiReleaseTagMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiReleaseTagMixin:interface)} functionality. + * @public + */ +export function ApiReleaseTagMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiReleaseTagMixin) { + class MixedClass extends baseClass implements ApiReleaseTagMixin { + public [_releaseTag]: ReleaseTag; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiReleaseTagMixinOptions = args[0]; + this[_releaseTag] = options.releaseTag; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiReleaseTagMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + const deserializedReleaseTag: ReleaseTag | undefined = Enum.tryGetValueByKey( + ReleaseTag as any, + jsonObject.releaseTag, + ); + if (deserializedReleaseTag === undefined) { + throw new Error(`Failed to deserialize release tag ${JSON.stringify(jsonObject.releaseTag)}`); + } + + options.releaseTag = deserializedReleaseTag; + } + + public get releaseTag(): ReleaseTag { + return this[_releaseTag]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.releaseTag = ReleaseTag[this.releaseTag]; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiReleaseTagMixin:interface)}. + * + * @public + */ +export namespace ApiReleaseTagMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReleaseTagMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReleaseTagMixin { + return apiItem.hasOwnProperty(_releaseTag); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiReturnTypeMixin.ts b/packages/api-extractor-model/src/mixins/ApiReturnTypeMixin.ts new file mode 100644 index 000000000..f11bb7780 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiReturnTypeMixin.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import type { IExcerptTokenRange, Excerpt } from './Excerpt.js'; + +/** + * Constructor options for {@link (ApiReturnTypeMixin:interface)}. + * + * @public + */ +export interface IApiReturnTypeMixinOptions extends IApiItemOptions { + returnTypeTokenRange: IExcerptTokenRange; +} + +export interface IApiReturnTypeMixinJson extends IApiItemJson { + returnTypeTokenRange: IExcerptTokenRange; +} + +const _returnTypeExcerpt: unique symbol = Symbol('ApiReturnTypeMixin._returnTypeExcerpt'); + +/** + * The mixin base class for API items that are functions that return a value. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiReturnTypeMixin extends ApiItem { + /** + * An {@link Excerpt} that describes the type of the function's return value. + */ + readonly returnTypeExcerpt: Excerpt; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiReturnTypeMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiReturnTypeMixin:interface)} functionality. + * @public + */ +export function ApiReturnTypeMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiReturnTypeMixin) { + class MixedClass extends baseClass implements ApiReturnTypeMixin { + public [_returnTypeExcerpt]: Excerpt; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiReturnTypeMixinOptions = args[0]; + + if (this instanceof ApiDeclaredItem) { + this[_returnTypeExcerpt] = this.buildExcerpt(options.returnTypeTokenRange); + } else { + throw new InternalError('ApiReturnTypeMixin expects a base class that inherits from ApiDeclaredItem'); + } + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiReturnTypeMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.returnTypeTokenRange = jsonObject.returnTypeTokenRange; + } + + public get returnTypeExcerpt(): Excerpt { + return this[_returnTypeExcerpt]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.returnTypeTokenRange = this.returnTypeExcerpt.tokenRange; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiReturnTypeMixin:interface)}. + * + * @public + */ +export namespace ApiReturnTypeMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReturnTypeMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReturnTypeMixin { + return apiItem.hasOwnProperty(_returnTypeExcerpt); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiStaticMixin.ts b/packages/api-extractor-model/src/mixins/ApiStaticMixin.ts new file mode 100644 index 000000000..82d156fab --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiStaticMixin.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; + +/** + * Constructor options for {@link (IApiStaticMixinOptions:interface)}. + * + * @public + */ +export interface IApiStaticMixinOptions extends IApiItemOptions { + isStatic: boolean; +} + +export interface IApiStaticMixinJson extends IApiItemJson { + isStatic: boolean; +} + +const _isStatic: unique symbol = Symbol('ApiStaticMixin._isStatic'); + +/** + * The mixin base class for API items that can have the TypeScript `static` keyword applied to them. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiStaticMixin extends ApiItem { + /** + * Whether the declaration has the TypeScript `static` keyword. + */ + readonly isStatic: boolean; + + /** + * @override + */ + serializeInto(jsonObject: Partial): void; +} + +/** + * Mixin function for {@link (ApiStaticMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiStaticMixin:interface)} functionality. + * @public + */ +export function ApiStaticMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiStaticMixin) { + class MixedClass extends baseClass implements ApiStaticMixin { + public [_isStatic]: boolean; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiStaticMixinOptions = args[0]; + this[_isStatic] = options.isStatic; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiStaticMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.isStatic = jsonObject.isStatic; + } + + public get isStatic(): boolean { + return this[_isStatic]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.isStatic = this.isStatic; + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiStaticMixin:interface)}. + * + * @public + */ +export namespace ApiStaticMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiStaticMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiStaticMixin { + return apiItem.hasOwnProperty(_isStatic); + } +} diff --git a/packages/api-extractor-model/src/mixins/ApiTypeParameterListMixin.ts b/packages/api-extractor-model/src/mixins/ApiTypeParameterListMixin.ts new file mode 100644 index 000000000..081834b19 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/ApiTypeParameterListMixin.ts @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js'; +import type { DeserializerContext } from '../model/DeserializerContext.js'; +import { TypeParameter } from '../model/TypeParameter.js'; +import type { Excerpt, IExcerptTokenRange } from './Excerpt.js'; + +/** + * Represents parameter information that is part of {@link IApiTypeParameterListMixinOptions} + * + * @public + */ +export interface IApiTypeParameterOptions { + constraintTokenRange: IExcerptTokenRange; + defaultTypeTokenRange: IExcerptTokenRange; + typeParameterName: string; +} + +/** + * Constructor options for {@link (ApiTypeParameterListMixin:interface)}. + * + * @public + */ +export interface IApiTypeParameterListMixinOptions extends IApiItemOptions { + typeParameters: IApiTypeParameterOptions[]; +} + +export interface IApiTypeParameterListMixinJson extends IApiItemJson { + typeParameters: IApiTypeParameterOptions[]; +} + +const _typeParameters: unique symbol = Symbol('ApiTypeParameterListMixin._typeParameters'); + +/** + * The mixin base class for API items that can have type parameters. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use + * TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various + * features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class + * to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components: + * the function that generates a subclass, an interface that describes the members of the subclass, and + * a namespace containing static members of the class. + * @public + */ + +export interface ApiTypeParameterListMixin extends ApiItem { + serializeInto(jsonObject: Partial): void; + + /** + * The type parameters. + */ + readonly typeParameters: readonly TypeParameter[]; +} + +/** + * Mixin function for {@link (ApiTypeParameterListMixin:interface)}. + * + * @param baseClass - The base class to be extended + * @returns A child class that extends baseClass, adding the {@link (ApiTypeParameterListMixin:interface)} + * functionality. + * @public + */ +export function ApiTypeParameterListMixin( + baseClass: TBaseClass, +): TBaseClass & (new (...args: any[]) => ApiTypeParameterListMixin) { + class MixedClass extends baseClass implements ApiTypeParameterListMixin { + public readonly [_typeParameters]: TypeParameter[]; + + public constructor(...args: any[]) { + super(...args); + + const options: IApiTypeParameterListMixinOptions = args[0]; + + this[_typeParameters] = []; + + if (this instanceof ApiDeclaredItem) { + if (options.typeParameters) { + for (const typeParameterOptions of options.typeParameters) { + const defaultTypeExcerpt: Excerpt = this.buildExcerpt(typeParameterOptions.defaultTypeTokenRange); + const typeParameter: TypeParameter = new TypeParameter({ + name: typeParameterOptions.typeParameterName, + constraintExcerpt: this.buildExcerpt(typeParameterOptions.constraintTokenRange), + defaultTypeExcerpt, + isOptional: !defaultTypeExcerpt.isEmpty, + parent: this, + }); + + this[_typeParameters].push(typeParameter); + } + } + } else { + throw new InternalError('ApiTypeParameterListMixin expects a base class that inherits from ApiDeclaredItem'); + } + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiTypeParameterListMixinJson, + ): void { + baseClass.onDeserializeInto(options, context, jsonObject); + + options.typeParameters = jsonObject.typeParameters || []; + } + + public get typeParameters(): readonly TypeParameter[] { + return this[_typeParameters]; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + const typeParameterObjects: IApiTypeParameterOptions[] = []; + for (const typeParameter of this.typeParameters) { + typeParameterObjects.push({ + typeParameterName: typeParameter.name, + constraintTokenRange: typeParameter.constraintExcerpt.tokenRange, + defaultTypeTokenRange: typeParameter.defaultTypeExcerpt.tokenRange, + }); + } + + if (typeParameterObjects.length > 0) { + jsonObject.typeParameters = typeParameterObjects; + } + } + } + + return MixedClass; +} + +/** + * Static members for {@link (ApiTypeParameterListMixin:interface)}. + * + * @public + */ +export namespace ApiTypeParameterListMixin { + /** + * A type guard that tests whether the specified `ApiItem` subclass extends the `ApiParameterListMixin` mixin. + * + * @remarks + * + * The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of + * the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however + * the TypeScript type system cannot invoke a runtime test.) + */ + export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiTypeParameterListMixin { + return apiItem.hasOwnProperty(_typeParameters); + } +} diff --git a/packages/api-extractor-model/src/mixins/Excerpt.ts b/packages/api-extractor-model/src/mixins/Excerpt.ts new file mode 100644 index 000000000..73213637c --- /dev/null +++ b/packages/api-extractor-model/src/mixins/Excerpt.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import { Text } from '@rushstack/node-core-library'; + +/** + * @public + */ +export enum ExcerptTokenKind { + /** + * Generic text without any special properties + */ + Content = 'Content', + + /** + * A reference to an API declaration + */ + Reference = 'Reference', +} + +/** + * Used by {@link Excerpt} to indicate a range of indexes within an array of `ExcerptToken` objects. + * + * @public + */ +export interface IExcerptTokenRange { + /** + * The index of the last member of the span, plus one. + * + * @remarks + * + * If `startIndex` and `endIndex` are the same number, then the span is empty. + */ + endIndex: number; + + /** + * The starting index of the span. + */ + startIndex: number; +} + +/** + * @public + */ +export interface IExcerptToken { + canonicalReference?: string | undefined; + readonly kind: ExcerptTokenKind; + text: string; +} + +/** + * Represents a fragment of text belonging to an {@link Excerpt} object. + * + * @public + */ +export class ExcerptToken { + private readonly _kind: ExcerptTokenKind; + + private readonly _text: string; + + private readonly _canonicalReference: DeclarationReference | undefined; + + public constructor(kind: ExcerptTokenKind, text: string, canonicalReference?: DeclarationReference) { + this._kind = kind; + + // Standardize the newlines across operating systems. Even though this may deviate from the actual + // input source file that was parsed, it's useful because the newline gets serialized inside + // a string literal in .api.json, which cannot be automatically normalized by Git. + this._text = Text.convertToLf(text); + this._canonicalReference = canonicalReference; + } + + /** + * Indicates the kind of token. + */ + public get kind(): ExcerptTokenKind { + return this._kind; + } + + /** + * The text fragment. + */ + public get text(): string { + return this._text; + } + + /** + * The hyperlink target for a token whose type is `ExcerptTokenKind.Reference`. For other token types, + * this property will be `undefined`. + */ + public get canonicalReference(): DeclarationReference | undefined { + return this._canonicalReference; + } +} + +/** + * The `Excerpt` class is used by {@link ApiDeclaredItem} to represent a TypeScript code fragment that may be + * annotated with hyperlinks to declared types (and in the future, source code locations). + * + * @remarks + * API Extractor's .api.json file format stores excerpts compactly as a start/end indexes into an array of tokens. + * Every `ApiDeclaredItem` has a "main excerpt" corresponding to the full list of tokens. The declaration may + * also have have "captured" excerpts that correspond to subranges of tokens. + * + * For example, if the main excerpt is: + * + * ``` + * function parse(s: string): Vector | undefined; + * ``` + * + * ...then this entire signature is the "main excerpt", whereas the function's return type `Vector | undefined` is a + * captured excerpt. The `Vector` token might be a hyperlink to that API item. + * + * An excerpt may be empty (i.e. a token range containing zero tokens). For example, if a function's return value + * is not explicitly declared, then the returnTypeExcerpt will be empty. By contrast, a class constructor cannot + * have a return value, so ApiConstructor has no returnTypeExcerpt property at all. + * @public + */ +export class Excerpt { + /** + * The complete list of tokens for the source code fragment that this excerpt is based upon. + * If this object is the main excerpt, then it will span all of the tokens; otherwise, it will correspond to + * a range within the array. + */ + public readonly tokens: readonly ExcerptToken[]; + + /** + * Specifies the excerpt's range within the `tokens` array. + */ + public readonly tokenRange: Readonly; + + /** + * The tokens spanned by this excerpt. It is the range of the `tokens` array as specified by the `tokenRange` + * property. + */ + public readonly spannedTokens: readonly ExcerptToken[]; + + private _text: string | undefined; + + public constructor(tokens: readonly ExcerptToken[], tokenRange: IExcerptTokenRange) { + this.tokens = tokens; + this.tokenRange = tokenRange; + + if ( + this.tokenRange.startIndex < 0 || + this.tokenRange.endIndex > this.tokens.length || + this.tokenRange.startIndex > this.tokenRange.endIndex + ) { + throw new Error('Invalid token range'); + } + + this.spannedTokens = this.tokens.slice(this.tokenRange.startIndex, this.tokenRange.endIndex); + } + + /** + * The excerpted text, formed by concatenating the text of the `spannedTokens` strings. + */ + public get text(): string { + if (this._text === undefined) { + this._text = this.spannedTokens.map((x) => x.text).join(''); + } + + return this._text; + } + + /** + * Returns true if the excerpt is an empty range. + */ + public get isEmpty(): boolean { + return this.tokenRange.startIndex === this.tokenRange.endIndex; + } +} diff --git a/packages/api-extractor-model/src/mixins/IFindApiItemsResult.ts b/packages/api-extractor-model/src/mixins/IFindApiItemsResult.ts new file mode 100644 index 000000000..ce503765d --- /dev/null +++ b/packages/api-extractor-model/src/mixins/IFindApiItemsResult.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ApiItem } from '../items/ApiItem.js'; + +/** + * Generic result object for finding API items used by different kinds of find operations. + * + * @public + */ +export interface IFindApiItemsResult { + /** + * The API items that were found. Not guaranteed to be complete, see `maybeIncompleteResult`. + */ + items: ApiItem[]; + + /** + * Indicates whether the result is potentially incomplete due to errors during the find operation. + * If true, the `messages` explain the errors in more detail. + */ + maybeIncompleteResult: boolean; + + /** + * Diagnostic messages regarding the find operation. + */ + messages: IFindApiItemsMessage[]; +} + +/** + * This object is used for messages returned as part of `IFindApiItemsResult`. + * + * @public + */ +export interface IFindApiItemsMessage { + /** + * Unique identifier for the message. + * + * @beta + */ + messageId: FindApiItemsMessageId; + + /** + * Text description of the message. + */ + text: string; +} + +/** + * Unique identifiers for messages returned as part of `IFindApiItemsResult`. + * + * @public + */ +export enum FindApiItemsMessageId { + /** + * "Unable to resolve declaration reference within API item ___: ___" + */ + DeclarationResolutionFailed = 'declaration-resolution-failed', + + /** + * "Unable to analyze extends clause ___ of API item ___ because no canonical reference was found." + */ + ExtendsClauseMissingReference = 'extends-clause-missing-reference', + + /** + * "Unable to analyze references of API item ___ because it is not associated with an ApiModel" + */ + NoAssociatedApiModel = 'no-associated-api-model', + + /** + * "Unable to analyze references of API item ___ because it is of unsupported kind ___" + */ + UnsupportedKind = 'unsupported-kind', +} diff --git a/packages/api-extractor-model/src/mixins/Mixin.ts b/packages/api-extractor-model/src/mixins/Mixin.ts new file mode 100644 index 000000000..069894ef3 --- /dev/null +++ b/packages/api-extractor-model/src/mixins/Mixin.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * This abstraction is used by the mixin pattern. + * It describes a class constructor. + * + * @public + */ +export type Constructor = new (...args: any[]) => T; + +/** + * This abstraction is used by the mixin pattern. + * It describes the "static side" of a class. + * + * @public + */ +export type PropertiesOf = { [K in keyof T]: T[K] }; diff --git a/packages/api-extractor-model/src/model/ApiCallSignature.ts b/packages/api-extractor-model/src/model/ApiCallSignature.ts new file mode 100644 index 000000000..cd1a6ccbb --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiCallSignature.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; +import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js'; +import { + type IApiTypeParameterListMixinOptions, + ApiTypeParameterListMixin, +} from '../mixins/ApiTypeParameterListMixin.js'; + +/** + * Constructor options for {@link ApiCallSignature}. + * + * @public + */ +export interface IApiCallSignatureOptions + extends IApiTypeParameterListMixinOptions, + IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript function call signature. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiCallSignature` represents a TypeScript declaration such as `(x: number, y: number): number` + * in this example: + * + * ```ts + * export interface IChooser { + * // A call signature: + * (x: number, y: number): number; + * + * // Another overload for this call signature: + * (x: string, y: string): string; + * } + * + * function chooseFirst(x: T, y: T): T { + * return x; + * } + * + * let chooser: IChooser = chooseFirst; + * ``` + * @public + */ +export class ApiCallSignature extends ApiTypeParameterListMixin( + ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiDeclaredItem))), +) { + public constructor(options: IApiCallSignatureOptions) { + super(options); + } + + public static getContainerKey(overloadIndex: number): string { + return `|${ApiItemKind.CallSignature}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.CallSignature; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiCallSignature.getContainerKey(this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const parent: DeclarationReference = this.parent + ? this.parent.canonicalReference + : // .withMeaning() requires some kind of component + DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)'); + return parent.withMeaning(Meaning.CallSignature as any).withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiClass.ts b/packages/api-extractor-model/src/model/ApiClass.ts new file mode 100644 index 000000000..3c92335fe --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiClass.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { + ApiAbstractMixin, + type IApiAbstractMixinJson, + type IApiAbstractMixinOptions, +} from '../mixins/ApiAbstractMixin.js'; +import { + type IApiExportedMixinJson, + type IApiExportedMixinOptions, + ApiExportedMixin, +} from '../mixins/ApiExportedMixin.js'; +import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import { + ApiTypeParameterListMixin, + type IApiTypeParameterListMixinOptions, + type IApiTypeParameterListMixinJson, +} from '../mixins/ApiTypeParameterListMixin.js'; +import type { IExcerptTokenRange } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from './DeserializerContext.js'; +import { HeritageType } from './HeritageType.js'; + +/** + * Constructor options for {@link ApiClass}. + * + * @public + */ +export interface IApiClassOptions + extends IApiItemContainerMixinOptions, + IApiNameMixinOptions, + IApiAbstractMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiTypeParameterListMixinOptions, + IApiExportedMixinOptions { + extendsTokenRange: IExcerptTokenRange | undefined; + implementsTokenRanges: IExcerptTokenRange[]; +} + +export interface IApiClassJson + extends IApiDeclaredItemJson, + IApiAbstractMixinJson, + IApiTypeParameterListMixinJson, + IApiExportedMixinJson { + extendsTokenRange?: IExcerptTokenRange; + implementsTokenRanges: IExcerptTokenRange[]; +} + +/** + * Represents a TypeScript class declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiClass` represents a TypeScript declaration such as this: + * + * ```ts + * export class X { } + * ``` + * @public + */ +export class ApiClass extends ApiItemContainerMixin( + ApiNameMixin(ApiAbstractMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))))), +) { + /** + * The base class that this class inherits from (using the `extends` keyword), or undefined if there is no base class. + */ + public readonly extendsType: HeritageType | undefined; + + private readonly _implementsTypes: HeritageType[] = []; + + public constructor(options: IApiClassOptions) { + super(options); + + if (options.extendsTokenRange) { + this.extendsType = new HeritageType(this.buildExcerpt(options.extendsTokenRange)); + } else { + this.extendsType = undefined; + } + + for (const implementsTokenRange of options.implementsTokenRanges) { + this._implementsTypes.push(new HeritageType(this.buildExcerpt(implementsTokenRange))); + } + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.Class}`; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiClassJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.extendsTokenRange = jsonObject.extendsTokenRange; + options.implementsTokenRanges = jsonObject.implementsTokenRanges; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Class; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiClass.getContainerKey(this.name); + } + + /** + * The list of interfaces that this class implements using the `implements` keyword. + */ + public get implementsTypes(): readonly HeritageType[] { + return this._implementsTypes; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + // Note that JSON does not support the "undefined" value, so we simply omit the field entirely if it is undefined + if (this.extendsType) { + jsonObject.extendsTokenRange = this.extendsType.excerpt.tokenRange; + } + + jsonObject.implementsTokenRanges = this.implementsTypes.map((x) => x.excerpt.tokenRange); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Class as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiConstructSignature.ts b/packages/api-extractor-model/src/model/ApiConstructSignature.ts new file mode 100644 index 000000000..de43165bc --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiConstructSignature.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; +import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js'; +import { + ApiTypeParameterListMixin, + type IApiTypeParameterListMixinOptions, +} from '../mixins/ApiTypeParameterListMixin.js'; + +/** + * Constructor options for {@link ApiConstructor}. + * + * @public + */ +export interface IApiConstructSignatureOptions + extends IApiTypeParameterListMixinOptions, + IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript construct signature that belongs to an `ApiInterface`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiConstructSignature` represents a construct signature using the `new` keyword such as in this example: + * + * ```ts + * export interface IVector { + * x: number; + * y: number; + * } + * + * export interface IVectorConstructor { + * // A construct signature: + * new(x: number, y: number): IVector; + * } + * + * export function createVector(vectorConstructor: IVectorConstructor, + * x: number, y: number): IVector { + * return new vectorConstructor(x, y); + * } + * + * class Vector implements IVector { + * public x: number; + * public y: number; + * public constructor(x: number, y: number) { + * this.x = x; + * this.y = y; + * } + * } + * + * let vector: Vector = createVector(Vector, 1, 2); + * ``` + * + * Compare with {@link ApiConstructor}, which describes the class constructor itself. + * @public + */ +export class ApiConstructSignature extends ApiTypeParameterListMixin( + ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiDeclaredItem))), +) { + public constructor(options: IApiConstructSignatureOptions) { + super(options); + } + + public static getContainerKey(overloadIndex: number): string { + return `|${ApiItemKind.ConstructSignature}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.ConstructSignature; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiConstructSignature.getContainerKey(this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const parent: DeclarationReference = this.parent + ? this.parent.canonicalReference + : // .withMeaning() requires some kind of component + DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)'); + return parent.withMeaning(Meaning.ConstructSignature as any).withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiConstructor.ts b/packages/api-extractor-model/src/model/ApiConstructor.ts new file mode 100644 index 000000000..ea62a8258 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiConstructor.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; + +/** + * Constructor options for {@link ApiConstructor}. + * + * @public + */ +export interface IApiConstructorOptions + extends IApiParameterListMixinOptions, + IApiProtectedMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript class constructor declaration that belongs to an `ApiClass`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiConstructor` represents a declaration using the `constructor` keyword such as in this example: + * + * ```ts + * export class Vector { + * public x: number; + * public y: number; + * + * // A class constructor: + * public constructor(x: number, y: number) { + * this.x = x; + * this.y = y; + * } + * } + * ``` + * + * Compare with {@link ApiConstructSignature}, which describes the construct signature for a class constructor. + * @public + */ +export class ApiConstructor extends ApiParameterListMixin(ApiProtectedMixin(ApiReleaseTagMixin(ApiDeclaredItem))) { + public constructor(options: IApiConstructorOptions) { + super(options); + } + + public static getContainerKey(overloadIndex: number): string { + return `|${ApiItemKind.Constructor}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Constructor; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiConstructor.getContainerKey(this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const parent: DeclarationReference = this.parent + ? this.parent.canonicalReference + : // .withMeaning() requires some kind of component + DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)'); + return parent.withMeaning(Meaning.Constructor as any).withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiEntryPoint.ts b/packages/api-extractor-model/src/model/ApiEntryPoint.ts new file mode 100644 index 000000000..b2fb34ac7 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiEntryPoint.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiItem, ApiItemKind } from '../items/ApiItem.js'; +import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiPackage } from './ApiPackage.js'; + +/** + * Constructor options for {@link ApiEntryPoint}. + * + * @public + */ +export interface IApiEntryPointOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions {} + +/** + * Represents the entry point for an NPM package. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiEntryPoint` represents the entry point to an NPM package. API Extractor does not currently support + * analysis of multiple entry points, but the `ApiEntryPoint` object is included to support a future feature. + * In the current implementation, `ApiEntryPoint.importPath` is always the empty string. + * + * For example, suppose the package.json file looks like this: + * + * ```json + * { + * "name": "example-library", + * "version": "1.0.0", + * "main": "./lib/index.js", + * "typings": "./lib/index.d.ts" + * } + * ``` + * + * In this example, the `ApiEntryPoint` would represent the TypeScript module for `./lib/index.js`. + * @public + */ +export class ApiEntryPoint extends ApiItemContainerMixin(ApiNameMixin(ApiItem)) { + public constructor(options: IApiEntryPointOptions) { + super(options); + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.EntryPoint; + } + + /** + * @override + */ + public override get containerKey(): string { + // No prefix needed, because ApiEntryPoint is the only possible member of an ApiPackage + return this.name; + } + + /** + * The module path for this entry point, relative to the parent `ApiPackage`. In the current implementation, + * this is always the empty string, indicating the default entry point. + * + * @remarks + * + * API Extractor does not currently support analysis of multiple entry points. If that feature is implemented + * in the future, then the `ApiEntryPoint.importPath` will be used to distinguish different entry points, + * for example: `controls/Button` in `import { Button } from "example-package/controls/Button";`. + * + * The `ApiEntryPoint.name` property stores the same value as `ApiEntryPoint.importPath`. + */ + public get importPath(): string { + return this.name; + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + if (this.parent instanceof ApiPackage) { + return DeclarationReference.package(this.parent.name, this.importPath); + } + + return DeclarationReference.empty(); + } +} diff --git a/packages/api-extractor-model/src/model/ApiEnum.ts b/packages/api-extractor-model/src/model/ApiEnum.ts new file mode 100644 index 000000000..ccb5e546c --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiEnum.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js'; +import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import type { ApiEnumMember } from './ApiEnumMember.js'; + +/** + * Constructor options for {@link ApiEnum}. + * + * @public + */ +export interface IApiEnumOptions + extends IApiItemContainerMixinOptions, + IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} + +/** + * Represents a TypeScript enum declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiEnum` represents an enum declaration such as `FontSizes` in the example below: + * + * ```ts + * export enum FontSizes { + * Small = 100, + * Medium = 200, + * Large = 300 + * } + * ``` + * @public + */ +export class ApiEnum extends ApiItemContainerMixin( + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))), +) { + public constructor(options: IApiEnumOptions) { + super(options); + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.Enum}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Enum; + } + + /** + * @override + */ + public override get members(): readonly ApiEnumMember[] { + return super.members as readonly ApiEnumMember[]; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiEnum.getContainerKey(this.name); + } + + /** + * @override + */ + public override addMember(member: ApiEnumMember): void { + if (member.kind !== ApiItemKind.EnumMember) { + throw new Error('Only ApiEnumMember objects can be added to an ApiEnum'); + } + + super.addMember(member); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Enum as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiEnumMember.ts b/packages/api-extractor-model/src/model/ApiEnumMember.ts new file mode 100644 index 000000000..4bb1844b4 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiEnumMember.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; + +/** + * Constructor options for {@link ApiEnumMember}. + * + * @public + */ +export interface IApiEnumMemberOptions + extends IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiInitializerMixinOptions {} + +/** + * Options for customizing the sort order of {@link ApiEnum} members. + * + * @privateRemarks + * This enum is currently only used by the `@microsoft/api-extractor` package; it is declared here + * because we anticipate that if more options are added in the future, their sorting will be implemented + * by the `@microsoft/api-extractor-model` package. + * + * See https://github.com/microsoft/rushstack/issues/918 for details. + * @public + */ +export enum EnumMemberOrder { + /** + * `ApiEnumMember` items are sorted according to their {@link ApiItem.getSortKey}. The order is + * basically alphabetical by identifier name, but otherwise unspecified to allow for cosmetic improvements. + * + * This is the default behavior. + */ + ByName = 'by-name', + + /** + * `ApiEnumMember` items preserve the original order of the declarations in the source file. + * (This disables the automatic sorting that is normally applied based on {@link ApiItem.getSortKey}.) + */ + Preserve = 'preserve', +} + +/** + * Represents a member of a TypeScript enum declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiEnumMember` represents an enum member such as `Small = 100` in the example below: + * + * ```ts + * export enum FontSizes { + * Small = 100, + * Medium = 200, + * Large = 300 + * } + * ``` + * @public + */ +export class ApiEnumMember extends ApiNameMixin(ApiReleaseTagMixin(ApiInitializerMixin(ApiDeclaredItem))) { + public constructor(options: IApiEnumMemberOptions) { + super(options); + } + + public static getContainerKey(name: string): string { + // No prefix needed, because ApiEnumMember is the only possible member of an ApiEnum + return name; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.EnumMember; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiEnumMember.getContainerKey(this.name); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(Navigation.Exports as any, nameComponent) + .withMeaning(Meaning.Member as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiFunction.ts b/packages/api-extractor-model/src/model/ApiFunction.ts new file mode 100644 index 000000000..9809fa7f7 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiFunction.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; +import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js'; +import { + type IApiTypeParameterListMixinOptions, + ApiTypeParameterListMixin, +} from '../mixins/ApiTypeParameterListMixin.js'; + +/** + * Constructor options for {@link ApiFunction}. + * + * @public + */ +export interface IApiFunctionOptions + extends IApiNameMixinOptions, + IApiTypeParameterListMixinOptions, + IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} + +/** + * Represents a TypeScript function declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiFunction` represents a TypeScript declaration such as this example: + * + * ```ts + * export function getAverage(x: number, y: number): number { + * return (x + y) / 2.0; + * } + * ``` + * + * Functions are exported by an entry point module or by a namespace. Compare with {@link ApiMethod}, which + * represents a function that is a member of a class. + * @public + */ +export class ApiFunction extends ApiNameMixin( + ApiTypeParameterListMixin( + ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiExportedMixin(ApiDeclaredItem)))), + ), +) { + public constructor(options: IApiFunctionOptions) { + super(options); + } + + public static getContainerKey(name: string, overloadIndex: number): string { + return `${name}|${ApiItemKind.Function}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Function; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiFunction.getContainerKey(this.name, this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Function as any) + .withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiIndexSignature.ts b/packages/api-extractor-model/src/model/ApiIndexSignature.ts new file mode 100644 index 000000000..7f24bf0c9 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiIndexSignature.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { type IApiReadonlyMixinOptions, ApiReadonlyMixin } from '../mixins/ApiReadonlyMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; +import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js'; + +/** + * Constructor options for {@link ApiIndexSignature}. + * + * @public + */ +export interface IApiIndexSignatureOptions + extends IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiReadonlyMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript index signature. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiIndexSignature` represents a TypeScript declaration such as `[x: number]: number` in this example: + * + * ```ts + * export interface INumberTable { + * // An index signature + * [value: number]: number; + * + * // An overloaded index signature + * [name: string]: number; + * } + * ``` + * @public + */ +export class ApiIndexSignature extends ApiParameterListMixin( + ApiReleaseTagMixin(ApiReturnTypeMixin(ApiReadonlyMixin(ApiDeclaredItem))), +) { + public constructor(options: IApiIndexSignatureOptions) { + super(options); + } + + public static getContainerKey(overloadIndex: number): string { + return `|${ApiItemKind.IndexSignature}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.IndexSignature; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiIndexSignature.getContainerKey(this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const parent: DeclarationReference = this.parent + ? this.parent.canonicalReference + : // .withMeaning() requires some kind of component + DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)'); + return parent.withMeaning(Meaning.IndexSignature as any).withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiInterface.ts b/packages/api-extractor-model/src/model/ApiInterface.ts new file mode 100644 index 000000000..150d03ce3 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiInterface.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { + type IApiExportedMixinJson, + type IApiExportedMixinOptions, + ApiExportedMixin, +} from '../mixins/ApiExportedMixin.js'; +import { + ApiItemContainerMixin, + type IApiItemContainerMixinOptions, + type IApiItemContainerJson, +} from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin, type IApiNameMixinJson } from '../mixins/ApiNameMixin.js'; +import { + type IApiReleaseTagMixinOptions, + ApiReleaseTagMixin, + type IApiReleaseTagMixinJson, +} from '../mixins/ApiReleaseTagMixin.js'; +import { + type IApiTypeParameterListMixinOptions, + type IApiTypeParameterListMixinJson, + ApiTypeParameterListMixin, +} from '../mixins/ApiTypeParameterListMixin.js'; +import type { IExcerptTokenRange } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from './DeserializerContext.js'; +import { HeritageType } from './HeritageType.js'; + +/** + * Constructor options for {@link ApiInterface}. + * + * @public + */ +export interface IApiInterfaceOptions + extends IApiItemContainerMixinOptions, + IApiNameMixinOptions, + IApiTypeParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiExportedMixinOptions { + extendsTokenRanges: IExcerptTokenRange[]; +} + +export interface IApiInterfaceJson + extends IApiItemContainerJson, + IApiNameMixinJson, + IApiTypeParameterListMixinJson, + IApiReleaseTagMixinJson, + IApiDeclaredItemJson, + IApiExportedMixinJson { + extendsTokenRanges: IExcerptTokenRange[]; +} + +/** + * Represents a TypeScript class declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiInterface` represents a TypeScript declaration such as this: + * + * ```ts + * export interface X extends Y { + * } + * ``` + * @public + */ +export class ApiInterface extends ApiItemContainerMixin( + ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem)))), +) { + private readonly _extendsTypes: HeritageType[] = []; + + public constructor(options: IApiInterfaceOptions) { + super(options); + + for (const extendsTokenRange of options.extendsTokenRanges) { + this._extendsTypes.push(new HeritageType(this.buildExcerpt(extendsTokenRange))); + } + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.Interface}`; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiInterfaceJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.extendsTokenRanges = jsonObject.extendsTokenRanges; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Interface; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiInterface.getContainerKey(this.name); + } + + /** + * The list of base interfaces that this interface inherits from using the `extends` keyword. + */ + public get extendsTypes(): readonly HeritageType[] { + return this._extendsTypes; + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.extendsTokenRanges = this.extendsTypes.map((x) => x.excerpt.tokenRange); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Interface as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiMethod.ts b/packages/api-extractor-model/src/model/ApiMethod.ts new file mode 100644 index 000000000..f379becad --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiMethod.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiAbstractMixinOptions, ApiAbstractMixin } from '../mixins/ApiAbstractMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js'; +import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js'; +import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js'; +import { ApiReturnTypeMixin, type IApiReturnTypeMixinOptions } from '../mixins/ApiReturnTypeMixin.js'; +import { ApiStaticMixin, type IApiStaticMixinOptions } from '../mixins/ApiStaticMixin.js'; +import { + ApiTypeParameterListMixin, + type IApiTypeParameterListMixinOptions, +} from '../mixins/ApiTypeParameterListMixin.js'; + +/** + * Constructor options for {@link ApiMethod}. + * + * @public + */ +export interface IApiMethodOptions + extends IApiNameMixinOptions, + IApiAbstractMixinOptions, + IApiOptionalMixinOptions, + IApiParameterListMixinOptions, + IApiProtectedMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiStaticMixinOptions, + IApiTypeParameterListMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript member function declaration that belongs to an `ApiClass`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiMethod` represents a TypeScript declaration such as the `render` member function in this example: + * + * ```ts + * export class Widget { + * public render(): void { } + * } + * ``` + * + * Compare with {@link ApiMethodSignature}, which represents a method belonging to an interface. + * For example, a class method can be `static` but an interface method cannot. + * @public + */ +export class ApiMethod extends ApiNameMixin( + ApiAbstractMixin( + ApiOptionalMixin( + ApiParameterListMixin( + ApiProtectedMixin( + ApiReleaseTagMixin(ApiReturnTypeMixin(ApiStaticMixin(ApiTypeParameterListMixin(ApiDeclaredItem)))), + ), + ), + ), + ), +) { + public constructor(options: IApiMethodOptions) { + super(options); + } + + public static getContainerKey(name: string, isStatic: boolean, overloadIndex: number): string { + if (isStatic) { + return `${name}|${ApiItemKind.Method}|static|${overloadIndex}`; + } else { + return `${name}|${ApiItemKind.Method}|instance|${overloadIndex}`; + } + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Method; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiMethod.getContainerKey(this.name, this.isStatic, this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep((this.isStatic ? Navigation.Exports : Navigation.Members) as any, nameComponent) + .withMeaning(Meaning.Member as any) + .withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiMethodSignature.ts b/packages/api-extractor-model/src/model/ApiMethodSignature.ts new file mode 100644 index 000000000..4baac8a8f --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiMethodSignature.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js'; +import { ApiParameterListMixin, type IApiParameterListMixinOptions } from '../mixins/ApiParameterListMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js'; +import { + type IApiTypeParameterListMixinOptions, + ApiTypeParameterListMixin, +} from '../mixins/ApiTypeParameterListMixin.js'; + +/** + * @public + */ +export interface IApiMethodSignatureOptions + extends IApiNameMixinOptions, + IApiTypeParameterListMixinOptions, + IApiParameterListMixinOptions, + IApiReleaseTagMixinOptions, + IApiReturnTypeMixinOptions, + IApiOptionalMixinOptions, + IApiDeclaredItemOptions {} + +/** + * Represents a TypeScript member function declaration that belongs to an `ApiInterface`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiMethodSignature` represents a TypeScript declaration such as the `render` member function in this example: + * + * ```ts + * export interface IWidget { + * render(): void; + * } + * ``` + * + * Compare with {@link ApiMethod}, which represents a method belonging to a class. + * For example, a class method can be `static` but an interface method cannot. + * @public + */ +export class ApiMethodSignature extends ApiNameMixin( + ApiTypeParameterListMixin( + ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiOptionalMixin(ApiDeclaredItem)))), + ), +) { + public constructor(options: IApiMethodSignatureOptions) { + super(options); + } + + public static getContainerKey(name: string, overloadIndex: number): string { + return `${name}|${ApiItemKind.MethodSignature}|${overloadIndex}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.MethodSignature; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiMethodSignature.getContainerKey(this.name, this.overloadIndex); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(Navigation.Members as any, nameComponent) + .withMeaning(Meaning.Member as any) + .withOverloadIndex(this.overloadIndex); + } +} diff --git a/packages/api-extractor-model/src/model/ApiModel.ts b/packages/api-extractor-model/src/model/ApiModel.ts new file mode 100644 index 000000000..67a1309d1 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiModel.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DocDeclarationReference } from '@microsoft/tsdoc'; +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { PackageName } from '@rushstack/node-core-library'; +import { ApiItem, ApiItemKind } from '../items/ApiItem.js'; +import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js'; +import { ApiPackage } from './ApiPackage.js'; +import { ModelReferenceResolver, type IResolveDeclarationReferenceResult } from './ModelReferenceResolver.js'; + +/** + * A serializable representation of a collection of API declarations. + * + * @remarks + * + * An `ApiModel` represents a collection of API declarations that can be serialized to disk. It captures all the + * important information needed to generate documentation, without any reliance on the TypeScript compiler engine. + * + * An `ApiModel` acts as the root of a tree of objects that all inherit from the `ApiItem` base class. + * The tree children are determined by the {@link (ApiItemContainerMixin:interface)} mixin base class. The model + * contains packages. Packages have an entry point (today, only one). And the entry point can contain various types + * of API declarations. The container relationships might look like this: + * + * ``` + * Things that can contain other things: + * + * - ApiModel + * - ApiPackage + * - ApiEntryPoint + * - ApiClass + * - ApiMethod + * - ApiProperty + * - ApiEnum + * - ApiEnumMember + * - ApiInterface + * - ApiMethodSignature + * - ApiPropertySignature + * - ApiNamespace + * - (ApiClass, ApiEnum, ApiInterace, ...) + * + * ``` + * + * Normally, API Extractor writes an .api.json file to disk for each project that it builds. Then, a tool like + * API Documenter can load the various `ApiPackage` objects into a single `ApiModel` and process them as a group. + * This is useful because compilation generally occurs separately (e.g. because projects may reside in different + * Git repos, or because they build with different TypeScript compiler configurations that may be incompatible), + * whereas API Documenter cannot detect broken hyperlinks without seeing the entire documentation set. + * @public + */ +export class ApiModel extends ApiItemContainerMixin(ApiItem) { + private readonly _resolver: ModelReferenceResolver; + + private _packagesByName: Map | undefined = undefined; + + private _apiItemsByCanonicalReference: Map | undefined = undefined; + + public constructor() { + super({}); + + this._resolver = new ModelReferenceResolver(this); + } + + public loadPackage(apiJsonFilename: string): ApiPackage { + const apiPackage: ApiPackage = ApiPackage.loadFromJsonFile(apiJsonFilename); + this.addMember(apiPackage); + return apiPackage; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Model; + } + + /** + * @override + */ + // eslint-disable-next-line @typescript-eslint/class-literal-property-style + public override get containerKey(): string { + return ''; + } + + public get packages(): readonly ApiPackage[] { + return this.members as readonly ApiPackage[]; + } + + /** + * @override + */ + public override addMember(member: ApiPackage): void { + if (member.kind !== ApiItemKind.Package) { + throw new Error('Only items of type ApiPackage may be added to an ApiModel'); + } + + super.addMember(member); + this._packagesByName = undefined; // invalidate the cache + this._apiItemsByCanonicalReference = undefined; // invalidate the cache + } + + /** + * Efficiently finds a package by the NPM package name. + * + * @remarks + * + * If the NPM scope is omitted in the package name, it will still be found provided that it is an unambiguous match. + * For example, it's often convenient to write `{@link node-core-library#JsonFile}` instead of + * `{@link @rushstack/node-core-library#JsonFile}`. + */ + public tryGetPackageByName(packageName: string): ApiPackage | undefined { + // Build the lookup on demand + if (this._packagesByName === undefined) { + this._packagesByName = new Map(); + + const unscopedMap: Map = new Map(); + + for (const apiPackage of this.packages) { + if (this._packagesByName.get(apiPackage.name)) { + // This should not happen + throw new Error(`The model contains multiple packages with the name ${apiPackage.name}`); + } + + this._packagesByName.set(apiPackage.name, apiPackage); + + const unscopedName: string = PackageName.parse(apiPackage.name).unscopedName; + + if (unscopedMap.has(unscopedName)) { + // If another package has the same unscoped name, then we won't register it + unscopedMap.set(unscopedName, undefined); + } else { + unscopedMap.set(unscopedName, apiPackage); + } + } + + for (const [unscopedName, apiPackage] of unscopedMap) { + if (apiPackage && !this._packagesByName.has(unscopedName)) { + // If the unscoped name is unambiguous, then we can also use it as a lookup + this._packagesByName.set(unscopedName, apiPackage); + } + } + } + + return this._packagesByName.get(packageName); + } + + public resolveDeclarationReference( + declarationReference: DeclarationReference | DocDeclarationReference, + contextApiItem: ApiItem | undefined, + ): IResolveDeclarationReferenceResult { + if (declarationReference instanceof DocDeclarationReference) { + return this._resolver.resolve(declarationReference, contextApiItem); + } else if (declarationReference instanceof DeclarationReference) { + // use this._apiItemsByCanonicalReference to look up ApiItem + + // Build the lookup on demand + if (!this._apiItemsByCanonicalReference) { + this._apiItemsByCanonicalReference = new Map(); + + for (const apiPackage of this.packages) { + this._initApiItemsRecursive(apiPackage, this._apiItemsByCanonicalReference); + } + } + + const result: IResolveDeclarationReferenceResult = { + resolvedApiItem: undefined, + errorMessage: undefined, + }; + + const apiItem: ApiItem | undefined = this._apiItemsByCanonicalReference.get(declarationReference.toString()); + + if (apiItem) { + result.resolvedApiItem = apiItem; + } else { + result.errorMessage = `${declarationReference.toString()} can not be located`; + } + + return result; + } else { + // NOTE: The "instanceof DeclarationReference" test assumes a specific version of the @microsoft/tsdoc package. + throw new TypeError( + 'The "declarationReference" parameter must be an instance of' + + ' DocDeclarationReference or DeclarationReference', + ); + } + } + + private _initApiItemsRecursive(apiItem: ApiItem, apiItemsByCanonicalReference: Map): void { + if (apiItem.canonicalReference && !apiItem.canonicalReference.isEmpty) { + apiItemsByCanonicalReference.set(apiItem.canonicalReference.toString(), apiItem); + } + + // Recurse container members + if (ApiItemContainerMixin.isBaseClassOf(apiItem)) { + for (const apiMember of apiItem.members) { + this._initApiItemsRecursive(apiMember, apiItemsByCanonicalReference); + } + } + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + return DeclarationReference.empty(); + } +} diff --git a/packages/api-extractor-model/src/model/ApiNamespace.ts b/packages/api-extractor-model/src/model/ApiNamespace.ts new file mode 100644 index 000000000..4dbdb8d76 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiNamespace.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js'; +import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; + +/** + * Constructor options for {@link ApiClass}. + * + * @public + */ +export interface IApiNamespaceOptions + extends IApiItemContainerMixinOptions, + IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiExportedMixinOptions {} + +/** + * Represents a TypeScript namespace declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiNamespace` represents a TypeScript declaration such `X` or `Y` in this example: + * + * ```ts + * export namespace X { + * export namespace Y { + * export interface IWidget { + * render(): void; + * } + * } + * } + * ``` + * @public + */ +export class ApiNamespace extends ApiItemContainerMixin( + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))), +) { + public constructor(options: IApiNamespaceOptions) { + super(options); + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.Namespace}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Namespace; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiNamespace.getContainerKey(this.name); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Namespace as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiPackage.ts b/packages/api-extractor-model/src/model/ApiPackage.ts new file mode 100644 index 000000000..6b684de7f --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiPackage.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { TSDocConfiguration } from '@microsoft/tsdoc'; +import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import { + JsonFile, + type IJsonFileSaveOptions, + PackageJsonLookup, + type IPackageJson, + type JsonObject, +} from '@rushstack/node-core-library'; +import { ApiDocumentedItem, type IApiDocumentedItemOptions } from '../items/ApiDocumentedItem.js'; +import { ApiItem, ApiItemKind, type IApiItemJson } from '../items/ApiItem.js'; +import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import type { ApiEntryPoint } from './ApiEntryPoint.js'; +import { DeserializerContext, ApiJsonSchemaVersion } from './DeserializerContext.js'; + +/** + * Constructor options for {@link ApiPackage}. + * + * @public + */ +export interface IApiPackageOptions + extends IApiItemContainerMixinOptions, + IApiNameMixinOptions, + IApiDocumentedItemOptions { + projectFolderUrl?: string | undefined; + tsdocConfiguration: TSDocConfiguration; +} + +export interface IApiPackageMetadataJson { + /** + * To support forwards compatibility, the `oldestForwardsCompatibleVersion` field tracks the oldest schema version + * whose corresponding deserializer could safely load this file. + * + * @remarks + * Normally api-extractor-model should refuse to load a schema version that is newer than the latest version + * that its deserializer understands. However, sometimes a schema change may merely introduce some new fields + * without modifying or removing any existing fields. In this case, an older api-extractor-model library can + * safely deserialize the newer version (by ignoring the extra fields that it doesn't recognize). The newer + * serializer can use this field to communicate that. + * + * If present, the `oldestForwardsCompatibleVersion` must be less than or equal to + * `IApiPackageMetadataJson.schemaVersion`. + */ + oldestForwardsCompatibleVersion?: ApiJsonSchemaVersion; + + /** + * The schema version for the .api.json file format. Used for determining whether the file format is + * supported, and for backwards compatibility. + */ + schemaVersion: ApiJsonSchemaVersion; + + /** + * The NPM package name for the tool that wrote the *.api.json file. + * For informational purposes only. + */ + toolPackage: string; + + /** + * The NPM package version for the tool that wrote the *.api.json file. + * For informational purposes only. + */ + toolVersion: string; + + /** + * The TSDoc configuration that was used when analyzing the API for this package. + * + * @remarks + * + * The structure of this objet is defined by the `@microsoft/tsdoc-config` library. + * Normally this configuration is loaded from the project's tsdoc.json file. It is stored + * in the .api.json file so that doc comments can be parsed accurately when loading the file. + */ + tsdocConfig: JsonObject; +} + +export interface IApiPackageJson extends IApiItemJson { + /** + * A file header that stores metadata about the tool that wrote the *.api.json file. + */ + metadata: IApiPackageMetadataJson; + + /** + * 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 `` path on disk. Provided via the + * `api-extractor.json` config. + */ + projectFolderUrl?: string; +} + +/** + * Options for {@link ApiPackage.saveToJsonFile}. + * + * @public + */ +export interface IApiPackageSaveOptions extends IJsonFileSaveOptions { + /** + * Set to true only when invoking API Extractor's test harness. + * + * @remarks + * When `testMode` is true, the `toolVersion` field in the .api.json file is assigned an empty string + * to prevent spurious diffs in output files tracked for tests. + */ + testMode?: boolean; + + /** + * Optionally specifies a value for the "toolPackage" field in the output .api.json data file; + * otherwise, the value will be "api-extractor-model". + */ + toolPackage?: string; + + /** + * Optionally specifies a value for the "toolVersion" field in the output .api.json data file; + * otherwise, the value will be the current version of the api-extractor-model package. + */ + toolVersion?: string; +} + +/** + * Represents an NPM package containing API declarations. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * @public + */ +export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumentedItem)) { + private readonly _tsdocConfiguration: TSDocConfiguration; + + private readonly _projectFolderUrl?: string | undefined; + + public constructor(options: IApiPackageOptions) { + super(options); + + this._tsdocConfiguration = options.tsdocConfiguration; + this._projectFolderUrl = options.projectFolderUrl; + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiPackageJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.projectFolderUrl = jsonObject.projectFolderUrl; + } + + public static loadFromJsonFile(apiJsonFilename: string): ApiPackage { + const jsonObject: IApiPackageJson = JsonFile.load(apiJsonFilename); + + if (!jsonObject?.metadata || typeof jsonObject.metadata.schemaVersion !== 'number') { + throw new Error( + `Error loading ${apiJsonFilename}:` + + `\nThe file format is not recognized; the "metadata.schemaVersion" field is missing or invalid`, + ); + } + + const schemaVersion: number = jsonObject.metadata.schemaVersion; + + if (schemaVersion < ApiJsonSchemaVersion.OLDEST_SUPPORTED) { + throw new Error( + `Error loading ${apiJsonFilename}:` + + `\nThe file format is version ${schemaVersion},` + + ` whereas ${ApiJsonSchemaVersion.OLDEST_SUPPORTED} is the oldest version supported by this tool`, + ); + } + + let oldestForwardsCompatibleVersion: number = schemaVersion; + if (jsonObject.metadata.oldestForwardsCompatibleVersion) { + // Sanity check + if (jsonObject.metadata.oldestForwardsCompatibleVersion > schemaVersion) { + throw new Error( + `Error loading ${apiJsonFilename}:` + + `\nInvalid file format; "oldestForwardsCompatibleVersion" cannot be newer than "schemaVersion"`, + ); + } + + oldestForwardsCompatibleVersion = jsonObject.metadata.oldestForwardsCompatibleVersion; + } + + let versionToDeserialize: number = schemaVersion; + if (versionToDeserialize > ApiJsonSchemaVersion.LATEST) { + // If the file format is too new, can we treat it as some earlier compatible version + // as indicated by oldestForwardsCompatibleVersion? + versionToDeserialize = Math.max(oldestForwardsCompatibleVersion, ApiJsonSchemaVersion.LATEST); + + if (versionToDeserialize > ApiJsonSchemaVersion.LATEST) { + // Nope, still too new + throw new Error( + `Error loading ${apiJsonFilename}:` + + `\nThe file format version ${schemaVersion} was written by a newer release of` + + ` the api-extractor-model library; you may need to upgrade your software`, + ); + } + } + + const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration(); + + if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004) { + const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig); + if (tsdocConfigFile.hasErrors) { + throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary()); + } + + tsdocConfigFile.configureParser(tsdocConfiguration); + } + + const context: DeserializerContext = new DeserializerContext({ + apiJsonFilename, + toolPackage: jsonObject.metadata.toolPackage, + toolVersion: jsonObject.metadata.toolVersion, + versionToDeserialize, + tsdocConfiguration, + }); + + return ApiItem.deserialize(jsonObject, context) as ApiPackage; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Package; + } + + /** + * @override + */ + public override get containerKey(): string { + // No prefix needed, because ApiPackage is the only possible member of an ApiModel + return this.name; + } + + public get entryPoints(): readonly ApiEntryPoint[] { + return this.members as readonly ApiEntryPoint[]; + } + + /** + * The TSDoc configuration that was used when analyzing the API for this package. + * + * @remarks + * + * Normally this configuration is loaded from the project's tsdoc.json file. It is stored + * in the .api.json file so that doc comments can be parsed accurately when loading the file. + */ + public get tsdocConfiguration(): TSDocConfiguration { + return this._tsdocConfiguration; + } + + public get projectFolderUrl(): string | undefined { + return this._projectFolderUrl; + } + + /** + * @override + */ + public override addMember(member: ApiEntryPoint): void { + if (member.kind !== ApiItemKind.EntryPoint) { + throw new Error('Only items of type ApiEntryPoint may be added to an ApiPackage'); + } + + super.addMember(member); + } + + public findEntryPointsByPath(importPath: string): readonly ApiEntryPoint[] { + return this.findMembersByName(importPath) as readonly ApiEntryPoint[]; + } + + public saveToJsonFile(apiJsonFilename: string, options?: IApiPackageSaveOptions): void { + const ioptions = options ?? {}; + + const packageJson: IPackageJson = PackageJsonLookup.loadOwnPackageJson(__dirname); + + const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(this.tsdocConfiguration); + const tsdocConfig: JsonObject = tsdocConfigFile.saveToObject(); + + const jsonObject: IApiPackageJson = { + metadata: { + toolPackage: ioptions.toolPackage ?? packageJson.name, + // In test mode, we don't write the real version, since that would cause spurious diffs whenever + // the version is bumped. Instead we write a placeholder string. + toolVersion: ioptions.testMode ? '[test mode]' : ioptions.toolVersion ?? packageJson.version, + schemaVersion: ApiJsonSchemaVersion.LATEST, + oldestForwardsCompatibleVersion: ApiJsonSchemaVersion.OLDEST_FORWARDS_COMPATIBLE, + tsdocConfig, + }, + } as IApiPackageJson; + + if (this.projectFolderUrl) { + jsonObject.projectFolderUrl = this.projectFolderUrl; + } + + this.serializeInto(jsonObject); + JsonFile.save(jsonObject, apiJsonFilename, ioptions); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + return DeclarationReference.package(this.name); + } +} diff --git a/packages/api-extractor-model/src/model/ApiProperty.ts b/packages/api-extractor-model/src/model/ApiProperty.ts new file mode 100644 index 000000000..df13ea750 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiProperty.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { ApiPropertyItem, type IApiPropertyItemOptions } from '../items/ApiPropertyItem.js'; +import { ApiAbstractMixin, type IApiAbstractMixinOptions } from '../mixins/ApiAbstractMixin.js'; +import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js'; +import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js'; +import { ApiStaticMixin, type IApiStaticMixinOptions } from '../mixins/ApiStaticMixin.js'; + +/** + * Constructor options for {@link ApiProperty}. + * + * @public + */ +export interface IApiPropertyOptions + extends IApiPropertyItemOptions, + IApiAbstractMixinOptions, + IApiProtectedMixinOptions, + IApiStaticMixinOptions, + IApiInitializerMixinOptions {} + +/** + * Represents a TypeScript property declaration that belongs to an `ApiClass`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiProperty` represents a TypeScript declaration such as the `width` and `height` members in this example: + * + * ```ts + * export class Widget { + * public width: number = 100; + * + * public get height(): number { + * if (this.isSquashed()) { + * return 0; + * } else { + * return this.clientArea.height; + * } + * } + * } + * ``` + * + * Note that member variables are also considered to be properties. + * + * If the property has both a getter function and setter function, they will be represented by a single `ApiProperty` + * and must have a single documentation comment. + * + * Compare with {@link ApiPropertySignature}, which represents a property belonging to an interface. + * For example, a class property can be `static` but an interface property cannot. + * @public + */ +export class ApiProperty extends ApiAbstractMixin( + ApiProtectedMixin(ApiStaticMixin(ApiInitializerMixin(ApiPropertyItem))), +) { + public constructor(options: IApiPropertyOptions) { + super(options); + } + + public static getContainerKey(name: string, isStatic: boolean): string { + if (isStatic) { + return `${name}|${ApiItemKind.Property}|static`; + } else { + return `${name}|${ApiItemKind.Property}|instance`; + } + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Property; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiProperty.getContainerKey(this.name, this.isStatic); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep((this.isStatic ? Navigation.Exports : Navigation.Members) as any, nameComponent) + .withMeaning(Meaning.Member as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiPropertySignature.ts b/packages/api-extractor-model/src/model/ApiPropertySignature.ts new file mode 100644 index 000000000..1a6566045 --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiPropertySignature.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { ApiPropertyItem, type IApiPropertyItemOptions } from '../items/ApiPropertyItem.js'; + +/** + * Constructor options for {@link ApiPropertySignature}. + * + * @public + */ +export interface IApiPropertySignatureOptions extends IApiPropertyItemOptions {} + +/** + * Represents a TypeScript property declaration that belongs to an `ApiInterface`. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiPropertySignature` represents a TypeScript declaration such as the `width` and `height` members in this example: + * + * ```ts + * export interface IWidget { + * readonly width: number; + * height: number; + * } + * ``` + * + * Compare with {@link ApiProperty}, which represents a property belonging to a class. + * For example, a class property can be `static` but an interface property cannot. + * @public + */ +export class ApiPropertySignature extends ApiPropertyItem { + public constructor(options: IApiPropertySignatureOptions) { + super(options); + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.PropertySignature}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.PropertySignature; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiPropertySignature.getContainerKey(this.name); + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(Navigation.Members as any, nameComponent) + .withMeaning(Meaning.Member as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiTypeAlias.ts b/packages/api-extractor-model/src/model/ApiTypeAlias.ts new file mode 100644 index 000000000..fc866a0af --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiTypeAlias.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { + type IApiExportedMixinJson, + type IApiExportedMixinOptions, + ApiExportedMixin, +} from '../mixins/ApiExportedMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import { + ApiTypeParameterListMixin, + type IApiTypeParameterListMixinOptions, + type IApiTypeParameterListMixinJson, +} from '../mixins/ApiTypeParameterListMixin.js'; +import type { Excerpt, IExcerptTokenRange } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from './DeserializerContext.js'; + +/** + * Constructor options for {@link ApiTypeAlias}. + * + * @public + */ +export interface IApiTypeAliasOptions + extends IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiDeclaredItemOptions, + IApiTypeParameterListMixinOptions, + IApiExportedMixinOptions { + typeTokenRange: IExcerptTokenRange; +} + +export interface IApiTypeAliasJson extends IApiDeclaredItemJson, IApiTypeParameterListMixinJson, IApiExportedMixinJson { + typeTokenRange: IExcerptTokenRange; +} + +/** + * Represents a TypeScript type alias declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiTypeAlias` represents a definition such as one of these examples: + * + * ```ts + * // A union type: + * export type Shape = Square | Triangle | Circle; + * + * // A generic type alias: + * export type BoxedValue = { value: T }; + * + * export type BoxedArray = { array: T[] }; + * + * // A conditional type alias: + * export type Boxed = T extends any[] ? BoxedArray : BoxedValue; + * + * ``` + * @public + */ +export class ApiTypeAlias extends ApiTypeParameterListMixin( + ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))), +) { + /** + * An {@link Excerpt} that describes the type of the alias. + * + * @remarks + * In the example below, the `typeExcerpt` would correspond to the subexpression + * `T extends any[] ? BoxedArray : BoxedValue;`: + * + * ```ts + * export type Boxed = T extends any[] ? BoxedArray : BoxedValue; + * ``` + */ + public readonly typeExcerpt: Excerpt; + + public constructor(options: IApiTypeAliasOptions) { + super(options); + + this.typeExcerpt = this.buildExcerpt(options.typeTokenRange); + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiTypeAliasJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.typeTokenRange = jsonObject.typeTokenRange; + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.TypeAlias}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.TypeAlias; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiTypeAlias.getContainerKey(this.name); + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.typeTokenRange = this.typeExcerpt.tokenRange; + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.TypeAlias as any); + } +} diff --git a/packages/api-extractor-model/src/model/ApiVariable.ts b/packages/api-extractor-model/src/model/ApiVariable.ts new file mode 100644 index 000000000..2b3a7438d --- /dev/null +++ b/packages/api-extractor-model/src/model/ApiVariable.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js'; +import { + type IApiExportedMixinJson, + type IApiExportedMixinOptions, + ApiExportedMixin, +} from '../mixins/ApiExportedMixin.js'; +import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js'; +import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js'; +import { ApiReadonlyMixin, type IApiReadonlyMixinOptions } from '../mixins/ApiReadonlyMixin.js'; +import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js'; +import type { IExcerptTokenRange, Excerpt } from '../mixins/Excerpt.js'; +import type { DeserializerContext } from './DeserializerContext.js'; + +/** + * Constructor options for {@link ApiVariable}. + * + * @public + */ +export interface IApiVariableOptions + extends IApiNameMixinOptions, + IApiReleaseTagMixinOptions, + IApiReadonlyMixinOptions, + IApiDeclaredItemOptions, + IApiInitializerMixinOptions, + IApiExportedMixinOptions { + variableTypeTokenRange: IExcerptTokenRange; +} + +export interface IApiVariableJson extends IApiDeclaredItemJson, IApiExportedMixinJson { + variableTypeTokenRange: IExcerptTokenRange; +} + +/** + * Represents a TypeScript variable declaration. + * + * @remarks + * + * This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of + * API declarations. + * + * `ApiVariable` represents an exported `const` or `let` object such as these examples: + * + * ```ts + * // A variable declaration + * export let verboseLogging: boolean; + * + * // A constant variable declaration with an initializer + * export const canvas: IWidget = createCanvas(); + * ``` + * @public + */ +export class ApiVariable extends ApiNameMixin( + ApiReleaseTagMixin(ApiReadonlyMixin(ApiInitializerMixin(ApiExportedMixin(ApiDeclaredItem)))), +) { + /** + * An {@link Excerpt} that describes the type of the variable. + */ + public readonly variableTypeExcerpt: Excerpt; + + public constructor(options: IApiVariableOptions) { + super(options); + + this.variableTypeExcerpt = this.buildExcerpt(options.variableTypeTokenRange); + } + + /** + * @override + */ + public static override onDeserializeInto( + options: Partial, + context: DeserializerContext, + jsonObject: IApiVariableJson, + ): void { + super.onDeserializeInto(options, context, jsonObject); + + options.variableTypeTokenRange = jsonObject.variableTypeTokenRange; + } + + public static getContainerKey(name: string): string { + return `${name}|${ApiItemKind.Variable}`; + } + + /** + * @override + */ + public override get kind(): ApiItemKind { + return ApiItemKind.Variable; + } + + /** + * @override + */ + public override get containerKey(): string { + return ApiVariable.getContainerKey(this.name); + } + + /** + * @override + */ + public override serializeInto(jsonObject: Partial): void { + super.serializeInto(jsonObject); + + jsonObject.variableTypeTokenRange = this.variableTypeExcerpt.tokenRange; + } + + /** + * @beta @override + */ + public override buildCanonicalReference(): DeclarationReference { + const nameComponent: Component = DeclarationReference.parseComponent(this.name); + const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals; + return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty()) + .addNavigationStep(navigation as any, nameComponent) + .withMeaning(Meaning.Variable as any); + } +} diff --git a/packages/api-extractor-model/src/model/Deserializer.ts b/packages/api-extractor-model/src/model/Deserializer.ts new file mode 100644 index 000000000..7433748b3 --- /dev/null +++ b/packages/api-extractor-model/src/model/Deserializer.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js'; +import { type IApiItemJson, type IApiItemOptions, type ApiItem, ApiItemKind } from '../items/ApiItem.js'; +import type { IApiPropertyItemJson } from '../items/ApiPropertyItem.js'; +import { ApiCallSignature, type IApiCallSignatureOptions } from './ApiCallSignature.js'; +import { ApiClass, type IApiClassOptions, type IApiClassJson } from './ApiClass.js'; +import { ApiConstructSignature, type IApiConstructSignatureOptions } from './ApiConstructSignature.js'; +import { ApiConstructor, type IApiConstructorOptions } from './ApiConstructor.js'; +import { ApiEntryPoint, type IApiEntryPointOptions } from './ApiEntryPoint.js'; +import { ApiEnum, type IApiEnumOptions } from './ApiEnum.js'; +import { ApiEnumMember, type IApiEnumMemberOptions } from './ApiEnumMember.js'; +import { ApiFunction, type IApiFunctionOptions } from './ApiFunction.js'; +import { ApiIndexSignature, type IApiIndexSignatureOptions } from './ApiIndexSignature.js'; +import { ApiInterface, type IApiInterfaceOptions, type IApiInterfaceJson } from './ApiInterface.js'; +import { ApiMethod, type IApiMethodOptions } from './ApiMethod.js'; +import { ApiMethodSignature, type IApiMethodSignatureOptions } from './ApiMethodSignature.js'; +import { ApiModel } from './ApiModel.js'; +import { ApiNamespace, type IApiNamespaceOptions } from './ApiNamespace.js'; +import { ApiPackage, type IApiPackageOptions, type IApiPackageJson } from './ApiPackage.js'; +import { ApiProperty, type IApiPropertyOptions } from './ApiProperty.js'; +import { ApiPropertySignature, type IApiPropertySignatureOptions } from './ApiPropertySignature.js'; +import { ApiTypeAlias, type IApiTypeAliasOptions, type IApiTypeAliasJson } from './ApiTypeAlias.js'; +import { ApiVariable, type IApiVariableOptions, type IApiVariableJson } from './ApiVariable.js'; +import type { DeserializerContext } from './DeserializerContext.js'; + +export class Deserializer { + public static deserialize(context: DeserializerContext, jsonObject: IApiItemJson): ApiItem { + const options: Partial = {}; + + switch (jsonObject.kind) { + case ApiItemKind.Class: + ApiClass.onDeserializeInto(options, context, jsonObject as IApiClassJson); + return new ApiClass(options as IApiClassOptions); + case ApiItemKind.CallSignature: + ApiCallSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiCallSignature(options as IApiCallSignatureOptions); + case ApiItemKind.Constructor: + ApiConstructor.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiConstructor(options as IApiConstructorOptions); + case ApiItemKind.ConstructSignature: + ApiConstructSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiConstructSignature(options as IApiConstructSignatureOptions); + case ApiItemKind.EntryPoint: + ApiEntryPoint.onDeserializeInto(options, context, jsonObject); + return new ApiEntryPoint(options as IApiEntryPointOptions); + case ApiItemKind.Enum: + ApiEnum.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiEnum(options as IApiEnumOptions); + case ApiItemKind.EnumMember: + ApiEnumMember.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiEnumMember(options as IApiEnumMemberOptions); + case ApiItemKind.Function: + ApiFunction.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiFunction(options as IApiFunctionOptions); + case ApiItemKind.IndexSignature: + ApiIndexSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiIndexSignature(options as IApiIndexSignatureOptions); + case ApiItemKind.Interface: + ApiInterface.onDeserializeInto(options, context, jsonObject as IApiInterfaceJson); + return new ApiInterface(options as IApiInterfaceOptions); + case ApiItemKind.Method: + ApiMethod.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiMethod(options as IApiMethodOptions); + case ApiItemKind.MethodSignature: + ApiMethodSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiMethodSignature(options as IApiMethodSignatureOptions); + case ApiItemKind.Model: + return new ApiModel(); + case ApiItemKind.Namespace: + ApiNamespace.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson); + return new ApiNamespace(options as IApiNamespaceOptions); + case ApiItemKind.Package: + ApiPackage.onDeserializeInto(options, context, jsonObject as IApiPackageJson); + return new ApiPackage(options as IApiPackageOptions); + case ApiItemKind.Property: + ApiProperty.onDeserializeInto(options, context, jsonObject as IApiPropertyItemJson); + return new ApiProperty(options as IApiPropertyOptions); + case ApiItemKind.PropertySignature: + ApiPropertySignature.onDeserializeInto(options, context, jsonObject as IApiPropertyItemJson); + return new ApiPropertySignature(options as IApiPropertySignatureOptions); + case ApiItemKind.TypeAlias: + ApiTypeAlias.onDeserializeInto(options, context, jsonObject as IApiTypeAliasJson); + return new ApiTypeAlias(options as IApiTypeAliasOptions); + case ApiItemKind.Variable: + ApiVariable.onDeserializeInto(options, context, jsonObject as IApiVariableJson); + return new ApiVariable(options as IApiVariableOptions); + default: + throw new Error(`Failed to deserialize unsupported API item type ${JSON.stringify(jsonObject.kind)}`); + } + } +} diff --git a/packages/api-extractor-model/src/model/DeserializerContext.ts b/packages/api-extractor-model/src/model/DeserializerContext.ts new file mode 100644 index 000000000..daf4de626 --- /dev/null +++ b/packages/api-extractor-model/src/model/DeserializerContext.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/prefer-literal-enum-member */ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { TSDocConfiguration } from '@microsoft/tsdoc'; + +export enum ApiJsonSchemaVersion { + /** + * The initial release. + */ + V_1000 = 1_000, + + /** + * Add support for type parameters and type alias types. + */ + V_1001 = 1_001, + + /** + * Remove `canonicalReference` field. This field was for diagnostic purposes only and was never deserialized. + */ + V_1002 = 1_002, + + /** + * Reintroduce the `canonicalReference` field using the experimental new TSDoc declaration reference notation. + * + * This is not a breaking change because this field is never deserialized; it is provided for informational + * purposes only. + */ + V_1003 = 1_003, + + /** + * Add a `tsdocConfig` field that tracks the TSDoc configuration for parsing doc comments. + * + * This is not a breaking change because an older implementation will still work correctly. The + * custom tags will be skipped over by the parser. + */ + V_1004 = 1_004, + + /** + * Add an `isOptional` field to `Parameter` and `TypeParameter` to track whether a function parameter is optional. + * + * When loading older JSON files, the value defaults to `false`. + */ + V_1005 = 1_005, + + /** + * Add an `isProtected` field to `ApiConstructor`, `ApiMethod`, and `ApiProperty` to + * track whether a class member has the `protected` modifier. + * + * Add an `isReadonly` field to `ApiProperty`, `ApiPropertySignature`, and `ApiVariable` to + * track whether the item is readonly. + * + * When loading older JSON files, the values default to `false`. + */ + V_1006 = 1_006, + + /** + * Add `ApiItemContainerMixin.preserveMemberOrder` to support enums that preserve their original sort order. + * + * When loading older JSON files, the value default to `false`. + */ + V_1007 = 1_007, + + /** + * Add an `initializerTokenRange` field to `ApiProperty` and `ApiVariable` to track the item's + * initializer. + * + * When loading older JSON files, this range is empty. + */ + V_1008 = 1_008, + + /** + * Add an `isReadonly` field to `ApiIndexSignature` to track whether the item is readonly. + * + * When loading older JSON files, the values defaults to `false`. + */ + V_1009 = 1_009, + + /** + * Add a `fileUrlPath` field to `ApiDeclaredItem` to track the URL to a declared item's source file. + * + * When loading older JSON files, the value defaults to `undefined`. + */ + V_1010 = 1_010, + + /** + * Add an `isAbstract` field to `ApiClass`, `ApiMethod`, and `ApiProperty` to + * track whether the item is abstract. + * + * When loading older JSON files, the value defaults to `false`. + */ + V_1011 = 1_011, + + /** + * The current latest .api.json schema version. + * + * IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE` + * should be updated. + */ + LATEST = V_1011, + + /** + * The oldest .api.json schema version that is still supported for backwards compatibility. + * + * This must be updated if you change to the file format and do not implement compatibility logic for + * deserializing the older representation. + */ + OLDEST_SUPPORTED = V_1001, + + /** + * Used to assign `IApiPackageMetadataJson.oldestForwardsCompatibleVersion`. + * + * This value must be \<= `ApiJsonSchemaVersion.LATEST`. It must be reset to the `LATEST` value + * if the older library would not be able to deserialize your new file format. Adding a nonessential field + * is generally okay. Removing, modifying, or reinterpreting existing fields is NOT safe. + */ + OLDEST_FORWARDS_COMPATIBLE = V_1001, +} + +export class DeserializerContext { + /** + * The path of the file being deserialized, which may be useful for diagnostic purposes. + */ + public readonly apiJsonFilename: string; + + /** + * Metadata from `IApiPackageMetadataJson.toolPackage`. + */ + public readonly toolPackage: string; + + /** + * Metadata from `IApiPackageMetadataJson.toolVersion`. + */ + public readonly toolVersion: string; + + /** + * The version of the schema being deserialized, as obtained from `IApiPackageMetadataJson.schemaVersion`. + */ + public readonly versionToDeserialize: ApiJsonSchemaVersion; + + /** + * The TSDoc configuration for the context. + */ + public readonly tsdocConfiguration: TSDocConfiguration; + + public constructor(options: DeserializerContext) { + this.apiJsonFilename = options.apiJsonFilename; + this.toolPackage = options.toolPackage; + this.toolVersion = options.toolVersion; + this.versionToDeserialize = options.versionToDeserialize; + this.tsdocConfiguration = options.tsdocConfiguration; + } +} diff --git a/packages/api-extractor-model/src/model/HeritageType.ts b/packages/api-extractor-model/src/model/HeritageType.ts new file mode 100644 index 000000000..a4fcfa2d9 --- /dev/null +++ b/packages/api-extractor-model/src/model/HeritageType.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { Excerpt } from '../mixins/Excerpt.js'; + +/** + * Represents a type referenced via an "extends" or "implements" heritage clause for a TypeScript class + * or interface. + * + * @remarks + * + * For example, consider this declaration: + * + * ```ts + * export class Widget extends Controls.WidgetBase implements Controls.IWidget, IDisposable { + * // . . . + * } + * ``` + * + * The heritage types are `Controls.WidgetBase`, `Controls.IWidget`, and `IDisposable`. + * @public + */ +export class HeritageType { + /** + * An excerpt corresponding to the referenced type. + * + * @remarks + * + * For example, consider this declaration: + * + * ```ts + * export class Widget extends Controls.WidgetBase implements Controls.IWidget, IDisposable { + * // . . . + * } + * ``` + * + * The excerpt might be `Controls.WidgetBase`, `Controls.IWidget`, or `IDisposable`. + */ + public readonly excerpt: Excerpt; + + public constructor(excerpt: Excerpt) { + this.excerpt = excerpt; + } +} diff --git a/packages/api-extractor-model/src/model/ModelReferenceResolver.ts b/packages/api-extractor-model/src/model/ModelReferenceResolver.ts new file mode 100644 index 000000000..972b57e81 --- /dev/null +++ b/packages/api-extractor-model/src/model/ModelReferenceResolver.ts @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { type DocDeclarationReference, type DocMemberSelector, SelectorKind } from '@microsoft/tsdoc'; +import { type ApiItem, ApiItemKind } from '../items/ApiItem.js'; +import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js'; +import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import type { ApiEntryPoint } from './ApiEntryPoint.js'; +import type { ApiModel } from './ApiModel.js'; +import type { ApiPackage } from './ApiPackage.js'; + +/** + * Result object for {@link ApiModel.resolveDeclarationReference}. + * + * @public + */ +export interface IResolveDeclarationReferenceResult { + /** + * If resolvedApiItem is undefined, then this will always contain an error message explaining why the + * resolution failed. + */ + errorMessage: string | undefined; + + /** + * The referenced ApiItem, if the declaration reference could be resolved. + */ + resolvedApiItem: ApiItem | undefined; +} + +/** + * This resolves a TSDoc declaration reference by walking the `ApiModel` hierarchy. + * + * @remarks + * + * This class is analogous to `AstReferenceResolver` from the `@microsoft/api-extractor` project, + * which resolves declaration references by walking the compiler state. + */ +export class ModelReferenceResolver { + private readonly _apiModel: ApiModel; + + public constructor(apiModel: ApiModel) { + this._apiModel = apiModel; + } + + public resolve( + declarationReference: DocDeclarationReference, + contextApiItem: ApiItem | undefined, + ): IResolveDeclarationReferenceResult { + const result: IResolveDeclarationReferenceResult = { + resolvedApiItem: undefined, + errorMessage: undefined, + }; + + let apiPackage: ApiPackage | undefined; + + // Is this an absolute reference? + if (declarationReference.packageName === undefined) { + // If the package name is omitted, try to infer it from the context + if (contextApiItem !== undefined) { + apiPackage = contextApiItem.getAssociatedPackage(); + } + + if (apiPackage === undefined) { + result.errorMessage = `The reference does not include a package name, and the package could not be inferred from the context`; + return result; + } + } else { + apiPackage = this._apiModel.tryGetPackageByName(declarationReference.packageName); + if (apiPackage === undefined) { + result.errorMessage = `The package "${declarationReference.packageName}" could not be located`; + return result; + } + } + + const importPath: string = declarationReference.importPath ?? ''; + + const foundEntryPoints: readonly ApiEntryPoint[] = apiPackage.findEntryPointsByPath(importPath); + if (foundEntryPoints.length < 1) { + result.errorMessage = `The import path "${importPath}" could not be resolved`; + return result; + } + + let currentItem: ApiItem = foundEntryPoints[0]!; + + // Now search for the member reference + for (const memberReference of declarationReference.memberReferences) { + if (memberReference.memberSymbol !== undefined) { + result.errorMessage = `Symbols are not yet supported in declaration references`; + return result; + } + + if (memberReference.memberIdentifier === undefined) { + result.errorMessage = `Missing member identifier`; + return result; + } + + const identifier: string = memberReference.memberIdentifier.identifier; + + if (!ApiItemContainerMixin.isBaseClassOf(currentItem)) { + // For example, {@link MyClass.myMethod.X} is invalid because methods cannot contain members + result.errorMessage = `Unable to resolve ${JSON.stringify( + identifier, + )} because ${currentItem.getScopedNameWithinPackage()} cannot act as a container`; + return result; + } + + const foundMembers: readonly ApiItem[] = currentItem.findMembersByName(identifier); + if (foundMembers.length === 0) { + result.errorMessage = `The member reference ${JSON.stringify(identifier)} was not found`; + return result; + } + + const memberSelector: DocMemberSelector | undefined = memberReference.selector; + if (memberSelector === undefined) { + if (foundMembers.length > 1) { + result.errorMessage = `The member reference ${JSON.stringify(identifier)} was ambiguous`; + return result; + } + + currentItem = foundMembers[0]!; + } else { + let memberSelectorResult: IResolveDeclarationReferenceResult; + switch (memberSelector.selectorKind) { + case SelectorKind.System: + memberSelectorResult = this._selectUsingSystemSelector(foundMembers, memberSelector, identifier); + break; + case SelectorKind.Index: + memberSelectorResult = this._selectUsingIndexSelector(foundMembers, memberSelector, identifier); + break; + default: + result.errorMessage = `The selector "${memberSelector.selector}" is not a supported selector type`; + return result; + } + + if (memberSelectorResult.resolvedApiItem === undefined) { + return memberSelectorResult; + } + + currentItem = memberSelectorResult.resolvedApiItem; + } + } + + result.resolvedApiItem = currentItem; + return result; + } + + private _selectUsingSystemSelector( + foundMembers: readonly ApiItem[], + memberSelector: DocMemberSelector, + identifier: string, + ): IResolveDeclarationReferenceResult { + const result: IResolveDeclarationReferenceResult = { + resolvedApiItem: undefined, + errorMessage: undefined, + }; + + const selectorName: string = memberSelector.selector; + + let selectorItemKind: ApiItemKind; + switch (selectorName) { + case 'class': + selectorItemKind = ApiItemKind.Class; + break; + case 'enum': + selectorItemKind = ApiItemKind.Enum; + break; + case 'function': + selectorItemKind = ApiItemKind.Function; + break; + case 'interface': + selectorItemKind = ApiItemKind.Interface; + break; + case 'namespace': + selectorItemKind = ApiItemKind.Namespace; + break; + case 'type': + selectorItemKind = ApiItemKind.TypeAlias; + break; + case 'variable': + selectorItemKind = ApiItemKind.Variable; + break; + default: + result.errorMessage = `Unsupported system selector "${selectorName}"`; + return result; + } + + const matches: ApiItem[] = foundMembers.filter((x) => x.kind === selectorItemKind); + if (matches.length === 0) { + result.errorMessage = `A declaration for "${identifier}" was not found that matches the TSDoc selector "${selectorName}"`; + return result; + } + + if (matches.length > 1) { + result.errorMessage = `More than one declaration "${identifier}" matches the TSDoc selector "${selectorName}"`; + } + + result.resolvedApiItem = matches[0]; + return result; + } + + private _selectUsingIndexSelector( + foundMembers: readonly ApiItem[], + memberSelector: DocMemberSelector, + identifier: string, + ): IResolveDeclarationReferenceResult { + const result: IResolveDeclarationReferenceResult = { + resolvedApiItem: undefined, + errorMessage: undefined, + }; + + const selectedMembers: ApiItem[] = []; + + const selectorOverloadIndex: number = Number.parseInt(memberSelector.selector, 10); + for (const foundMember of foundMembers) { + if (ApiParameterListMixin.isBaseClassOf(foundMember) && foundMember.overloadIndex === selectorOverloadIndex) { + selectedMembers.push(foundMember); + } + } + + if (selectedMembers.length === 0) { + result.errorMessage = + `An overload for ${JSON.stringify(identifier)} was not found that matches` + + ` the TSDoc selector ":${selectorOverloadIndex}"`; + return result; + } + + if (selectedMembers.length === 1) { + result.resolvedApiItem = selectedMembers[0]; + return result; + } + + result.errorMessage = `The member reference ${JSON.stringify(identifier)} was ambiguous`; + return result; + } +} diff --git a/packages/api-extractor-model/src/model/Parameter.ts b/packages/api-extractor-model/src/model/Parameter.ts new file mode 100644 index 000000000..c0d47f0a6 --- /dev/null +++ b/packages/api-extractor-model/src/model/Parameter.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import { ApiDocumentedItem } from '../items/ApiDocumentedItem.js'; +import type { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js'; +import type { Excerpt } from '../mixins/Excerpt.js'; + +/** + * Constructor options for {@link Parameter}. + * + * @public + */ +export interface IParameterOptions { + isOptional: boolean; + isRest: boolean; + name: string; + parameterTypeExcerpt: Excerpt; + parent: ApiParameterListMixin; +} + +/** + * Represents a named parameter for a function-like declaration. + * + * @remarks + * + * `Parameter` represents a TypeScript declaration such as `x: number` in this example: + * + * ```ts + * export function add(x: number, y: number): number { + * return x + y; + * } + * ``` + * + * `Parameter` objects belong to the {@link (ApiParameterListMixin:interface).parameters} collection. + * @public + */ +export class Parameter { + /** + * An {@link Excerpt} that describes the type of the parameter. + */ + public readonly parameterTypeExcerpt: Excerpt; + + /** + * The parameter name. + */ + public name: string; + + /** + * Whether the parameter is optional. + */ + public isOptional: boolean; + + /** + * Whether the parameter is a rest parameter + */ + public isRest: boolean; + + private readonly _parent: ApiParameterListMixin; + + public constructor(options: IParameterOptions) { + this.name = options.name; + this.parameterTypeExcerpt = options.parameterTypeExcerpt; + this.isOptional = options.isOptional; + this.isRest = options.isRest; + this._parent = options.parent; + } + + /** + * Returns the `@param` documentation for this parameter, if present. + */ + public get tsdocParamBlock(): tsdoc.DocParamBlock | undefined { + if (this._parent instanceof ApiDocumentedItem && this._parent.tsdocComment) { + return this._parent.tsdocComment.params.tryGetBlockByName(this.name); + } + + return undefined; + } +} diff --git a/packages/api-extractor-model/src/model/SourceLocation.ts b/packages/api-extractor-model/src/model/SourceLocation.ts new file mode 100644 index 000000000..fa5e41365 --- /dev/null +++ b/packages/api-extractor-model/src/model/SourceLocation.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { URL } from 'node:url'; + +/** + * Constructor options for `SourceLocation`. + * + * @public + */ +export interface ISourceLocationOptions { + /** + * The file URL path relative to the `projectFolder` and `projectFolderURL` fields as + * defined in the `api-extractor.json` config. + */ + fileUrlPath?: string | undefined; + + /** + * The project folder URL as defined by the `api-extractor.json` config `projectFolderUrl` + * setting. + */ + projectFolderUrl?: string | undefined; + + /** + * The column number in the source file. The first column number is 1. + */ + sourceFileColumn?: number | undefined; + + /** + * The line number in the source file. The first line number is 1. + */ + sourceFileLine?: number | undefined; +} + +/** + * The source location where a given API item is declared. + * + * @remarks + * The source location points to the `.ts` source file where the API item was originally + * declared. However, in some cases, if source map resolution fails, it falls back to pointing + * to the `.d.ts` file instead. + * @public + */ +export class SourceLocation { + private readonly _projectFolderUrl?: string | undefined; + + private readonly _fileUrlPath?: string | undefined; + + private readonly _fileLine?: number | undefined; + + private readonly _fileColumn?: number | undefined; + + public constructor(options: ISourceLocationOptions) { + this._projectFolderUrl = options.projectFolderUrl; + this._fileUrlPath = options.fileUrlPath; + this._fileLine = options.sourceFileLine; + this._fileColumn = options.sourceFileColumn; + } + + /** + * Returns the file URL to the given source location. Returns `undefined` if the file URL + * cannot be determined. + */ + public get fileUrl(): string | undefined { + if (this._projectFolderUrl === undefined || this._fileUrlPath === undefined) { + return undefined; + } + + let projectFolderUrl: string = this._projectFolderUrl; + if (!projectFolderUrl.endsWith('/')) { + projectFolderUrl += '/'; + } + + const url: URL = new URL(this._fileUrlPath, projectFolderUrl); + return url.href; + } + + /** + * The line in the `fileUrlPath` where the API item is declared. + */ + public get fileLine(): number | undefined { + return this._fileLine; + } + + /** + * The column in the `fileUrlPath` where the API item is declared. + */ + public get fileColumn(): number | undefined { + return this._fileColumn; + } +} diff --git a/packages/api-extractor-model/src/model/TypeParameter.ts b/packages/api-extractor-model/src/model/TypeParameter.ts new file mode 100644 index 000000000..cbd32986e --- /dev/null +++ b/packages/api-extractor-model/src/model/TypeParameter.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import { ApiDocumentedItem } from '../items/ApiDocumentedItem.js'; +import type { ApiTypeParameterListMixin } from '../mixins/ApiTypeParameterListMixin.js'; +import type { Excerpt } from '../mixins/Excerpt.js'; + +/** + * Constructor options for {@link TypeParameter}. + * + * @public + */ +export interface ITypeParameterOptions { + constraintExcerpt: Excerpt; + defaultTypeExcerpt: Excerpt; + isOptional: boolean; + name: string; + parent: ApiTypeParameterListMixin; +} + +/** + * Represents a named type parameter for a generic declaration. + * + * @remarks + * + * `TypeParameter` represents a TypeScript declaration such as `T` in this example: + * + * ```ts + * interface IIdentifier { + * getCode(): string; + * } + * + * class BarCode implements IIdentifier { + * private _value: number; + * public getCode(): string { return this._value.toString(); } + * } + * + * class Book { + * public identifier: TIdentifier; + * } + * ``` + * + * `TypeParameter` objects belong to the {@link (ApiTypeParameterListMixin:interface).typeParameters} collection. + * @public + */ +export class TypeParameter { + /** + * An {@link Excerpt} that describes the base constraint of the type parameter. + * + * @remarks + * In the example below, the `constraintExcerpt` would correspond to the `IIdentifier` subexpression: + * + * ```ts + * class Book { + * public identifier: TIdentifier; + * } + * ``` + */ + public readonly constraintExcerpt: Excerpt; + + /** + * An {@link Excerpt} that describes the default type of the type parameter. + * + * @remarks + * In the example below, the `defaultTypeExcerpt` would correspond to the `BarCode` subexpression: + * + * ```ts + * class Book { + * public identifier: TIdentifier; + * } + * ``` + */ + public readonly defaultTypeExcerpt: Excerpt; + + /** + * The parameter name. + */ + public name: string; + + /** + * Whether the type parameter is optional. True IFF there exists a `defaultTypeExcerpt`. + */ + public isOptional: boolean; + + private readonly _parent: ApiTypeParameterListMixin; + + public constructor(options: ITypeParameterOptions) { + this.name = options.name; + this.constraintExcerpt = options.constraintExcerpt; + this.defaultTypeExcerpt = options.defaultTypeExcerpt; + this.isOptional = options.isOptional; + this._parent = options.parent; + } + + /** + * Returns the `@typeParam` documentation for this parameter, if present. + */ + public get tsdocTypeParamBlock(): tsdoc.DocParamBlock | undefined { + if (this._parent instanceof ApiDocumentedItem && this._parent.tsdocComment) { + return this._parent.tsdocComment.typeParams.tryGetBlockByName(this.name); + } + + return undefined; + } +} diff --git a/packages/api-extractor-model/tsconfig.eslint.json b/packages/api-extractor-model/tsconfig.eslint.json new file mode 100644 index 000000000..0aa3b9660 --- /dev/null +++ b/packages/api-extractor-model/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src"] +} diff --git a/packages/api-extractor-model/tsconfig.json b/packages/api-extractor-model/tsconfig.json new file mode 100644 index 000000000..8dba547fb --- /dev/null +++ b/packages/api-extractor-model/tsconfig.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + + "compilerOptions": { + "types": ["jest", "node"], + "isolatedModules": false + } +} diff --git a/packages/api-extractor-model/tsup.config.ts b/packages/api-extractor-model/tsup.config.ts new file mode 100644 index 000000000..2e679fd0a --- /dev/null +++ b/packages/api-extractor-model/tsup.config.ts @@ -0,0 +1,3 @@ +import { createTsupConfig } from '../../tsup.config.js'; + +export default createTsupConfig(); diff --git a/packages/api-extractor-utils/package.json b/packages/api-extractor-utils/package.json index a40106b33..4761ef7d2 100644 --- a/packages/api-extractor-utils/package.json +++ b/packages/api-extractor-utils/package.json @@ -45,7 +45,7 @@ }, "homepage": "https://discord.js.org", "dependencies": { - "@microsoft/api-extractor-model": "7.28.2", + "@discordjs/api-extractor-model": "workspace:^", "@microsoft/tsdoc": "0.14.2" }, "devDependencies": { diff --git a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts index e237272b5..1029f059e 100644 --- a/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts +++ b/packages/api-extractor-utils/src/ApiNodeJSONEncoder.ts @@ -16,8 +16,8 @@ import type { ApiConstructor, ApiItemContainerMixin, ApiReturnTypeMixin, -} from '@microsoft/api-extractor-model'; -import { ApiDeclaredItem, ApiItemKind } from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; +import { ApiDeclaredItem, ApiItemKind } from '@discordjs/api-extractor-model'; import { generateTypeParamData } from './TypeParameterJSONEncoder.js'; import { type TokenDocumentation, resolveName, genReference, genToken, genParameter, generatePath } from './parse.js'; import type { DocBlockJSON } from './tsdoc/CommentBlock.js'; diff --git a/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts b/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts index d3c528df0..bbeadfa0b 100644 --- a/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts +++ b/packages/api-extractor-utils/src/TypeParameterJSONEncoder.ts @@ -1,4 +1,4 @@ -import type { TypeParameter, ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import type { TypeParameter, ApiModel, ApiItem } from '@discordjs/api-extractor-model'; import { type TokenDocumentation, genToken } from './parse.js'; import { type DocBlockJSON, block } from './tsdoc/CommentBlock.js'; diff --git a/packages/api-extractor-utils/src/parse.ts b/packages/api-extractor-utils/src/parse.ts index 830bc9e57..7514f60f8 100644 --- a/packages/api-extractor-utils/src/parse.ts +++ b/packages/api-extractor-utils/src/parse.ts @@ -11,7 +11,7 @@ import { type Parameter, type ApiFunction, ApiDeclaredItem, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; import type { DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc'; import { type Meaning, ModuleSource } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; import type { DocBlockJSON } from './tsdoc/CommentBlock.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts b/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts index 09b4de753..6535a4985 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentBlock.ts @@ -1,4 +1,4 @@ -import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import type { ApiModel, ApiItem } from '@discordjs/api-extractor-model'; import type { DocBlock } from '@microsoft/tsdoc'; import { blockTag, type DocBlockTagJSON } from './CommentBlockTag.js'; import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts b/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts index 28c43e86f..503a66e88 100644 --- a/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts +++ b/packages/api-extractor-utils/src/tsdoc/CommentNodeContainer.ts @@ -1,4 +1,4 @@ -import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model'; import type { DocNodeContainer } from '@microsoft/tsdoc'; import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js'; import { createCommentNode } from './index.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts b/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts index c1004bfb3..16383c134 100644 --- a/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts +++ b/packages/api-extractor-utils/src/tsdoc/LinkTagCommentNode.ts @@ -1,4 +1,4 @@ -import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model'; import type { DocDeclarationReference, DocLinkTag } from '@microsoft/tsdoc'; import { resolveName, generatePath } from '../parse.js'; import { type DocNodeJSON, node } from './CommentNode.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts b/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts index 8e9010dea..63c22581d 100644 --- a/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts +++ b/packages/api-extractor-utils/src/tsdoc/ParamBlock.ts @@ -1,4 +1,4 @@ -import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model'; import type { DocParamBlock } from '@microsoft/tsdoc'; import { block, type DocBlockJSON } from './CommentBlock.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/RootComment.ts b/packages/api-extractor-utils/src/tsdoc/RootComment.ts index cf503d296..69c7db52d 100644 --- a/packages/api-extractor-utils/src/tsdoc/RootComment.ts +++ b/packages/api-extractor-utils/src/tsdoc/RootComment.ts @@ -1,4 +1,4 @@ -import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model'; +import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model'; import type { DocComment } from '@microsoft/tsdoc'; import { block, type DocBlockJSON } from './CommentBlock.js'; import { type DocNodeJSON, node } from './CommentNode.js'; diff --git a/packages/api-extractor-utils/src/tsdoc/index.ts b/packages/api-extractor-utils/src/tsdoc/index.ts index 3784656fe..5a55d6440 100644 --- a/packages/api-extractor-utils/src/tsdoc/index.ts +++ b/packages/api-extractor-utils/src/tsdoc/index.ts @@ -1,4 +1,4 @@ -import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import type { ApiModel, ApiItem } from '@discordjs/api-extractor-model'; import { type DocNode, type DocPlainText, diff --git a/packages/api-extractor/.gitignore b/packages/api-extractor/.gitignore new file mode 100644 index 000000000..8150f3511 --- /dev/null +++ b/packages/api-extractor/.gitignore @@ -0,0 +1,23 @@ +# Packages +node_modules + +# Log files +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Env +.env + +# Dist +dist + +# Miscellaneous +.turbo +.tmp +coverage diff --git a/packages/api-extractor/.lintstagedrc.js b/packages/api-extractor/.lintstagedrc.js new file mode 100644 index 000000000..dc17706a5 --- /dev/null +++ b/packages/api-extractor/.lintstagedrc.js @@ -0,0 +1 @@ +module.exports = require('../../.lintstagedrc.json'); diff --git a/packages/api-extractor/.npmignore b/packages/api-extractor/.npmignore new file mode 100644 index 000000000..7a29489cb --- /dev/null +++ b/packages/api-extractor/.npmignore @@ -0,0 +1,32 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README (and its variants) +# CHANGELOG (and its variants) +# LICENSE / LICENCE + +#-------------------------------------------- +# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE +#-------------------------------------------- + +# (Add your project-specific overrides here) + +!/extends/*.json diff --git a/packages/api-extractor/.prettierignore b/packages/api-extractor/.prettierignore new file mode 100644 index 000000000..ee038b792 --- /dev/null +++ b/packages/api-extractor/.prettierignore @@ -0,0 +1,5 @@ +.turbo +coverage +dist +CHANGELOG.md +tsup.config.bundled* diff --git a/packages/api-extractor/.prettierrc.js b/packages/api-extractor/.prettierrc.js new file mode 100644 index 000000000..f004026c7 --- /dev/null +++ b/packages/api-extractor/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../.prettierrc.json'); diff --git a/packages/api-extractor/CHANGELOG.json b/packages/api-extractor/CHANGELOG.json new file mode 100644 index 000000000..aee451cef --- /dev/null +++ b/packages/api-extractor/CHANGELOG.json @@ -0,0 +1,7028 @@ +{ + "name": "@microsoft/api-extractor", + "entries": [ + { + "version": "7.38.1", + "tag": "@microsoft/api-extractor_v7.38.1", + "date": "Mon, 30 Oct 2023 23:36:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.17.0`" + } + ] + } + }, + { + "version": "7.38.0", + "tag": "@microsoft/api-extractor_v7.38.0", + "date": "Sun, 01 Oct 2023 02:56:29 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new message \"ae-undocumented\" to support logging of undocumented API items" + } + ] + } + }, + { + "version": "7.37.3", + "tag": "@microsoft/api-extractor_v7.37.3", + "date": "Sat, 30 Sep 2023 00:20:51 GMT", + "comments": { + "patch": [ + { + "comment": "Don't strip out @alpha items when generating API reports." + } + ] + } + }, + { + "version": "7.37.2", + "tag": "@microsoft/api-extractor_v7.37.2", + "date": "Thu, 28 Sep 2023 20:53:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.61.0`" + } + ] + } + }, + { + "version": "7.37.1", + "tag": "@microsoft/api-extractor_v7.37.1", + "date": "Tue, 26 Sep 2023 09:30:33 GMT", + "comments": { + "patch": [ + { + "comment": "Update type-only imports to include the type modifier." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.1`" + } + ] + } + }, + { + "version": "7.37.0", + "tag": "@microsoft/api-extractor_v7.37.0", + "date": "Fri, 15 Sep 2023 00:36:58 GMT", + "comments": { + "minor": [ + { + "comment": "Update @types/node from 14 to 18" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.28.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.60.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.5.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.16.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.4`" + } + ] + } + }, + { + "version": "7.36.4", + "tag": "@microsoft/api-extractor_v7.36.4", + "date": "Tue, 08 Aug 2023 07:10:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.7`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.3`" + } + ] + } + }, + { + "version": "7.36.3", + "tag": "@microsoft/api-extractor_v7.36.3", + "date": "Wed, 19 Jul 2023 00:20:31 GMT", + "comments": { + "patch": [ + { + "comment": "Updated semver dependency" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.6`" + } + ] + } + }, + { + "version": "7.36.2", + "tag": "@microsoft/api-extractor_v7.36.2", + "date": "Wed, 12 Jul 2023 15:20:39 GMT", + "comments": { + "patch": [ + { + "comment": "Add api-extractor support for .d.mts and .d.cts files" + } + ] + } + }, + { + "version": "7.36.1", + "tag": "@microsoft/api-extractor_v7.36.1", + "date": "Thu, 06 Jul 2023 00:16:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.5`" + } + ] + } + }, + { + "version": "7.36.0", + "tag": "@microsoft/api-extractor_v7.36.0", + "date": "Mon, 19 Jun 2023 22:40:21 GMT", + "comments": { + "minor": [ + { + "comment": "Use the `IRigConfig` interface in the `IExtractorConfigLoadForFolderOptions` object insteacd of the `RigConfig` class." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.4.0`" + } + ] + } + }, + { + "version": "7.35.4", + "tag": "@microsoft/api-extractor_v7.35.4", + "date": "Thu, 15 Jun 2023 00:21:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.4`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.21`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.2`" + } + ] + } + }, + { + "version": "7.35.3", + "tag": "@microsoft/api-extractor_v7.35.3", + "date": "Tue, 13 Jun 2023 01:49:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.15.0`" + } + ] + } + }, + { + "version": "7.35.2", + "tag": "@microsoft/api-extractor_v7.35.2", + "date": "Wed, 07 Jun 2023 22:45:16 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.20`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.14.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.1`" + } + ] + } + }, + { + "version": "7.35.1", + "tag": "@microsoft/api-extractor_v7.35.1", + "date": "Mon, 29 May 2023 15:21:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.2`" + } + ] + } + }, + { + "version": "7.35.0", + "tag": "@microsoft/api-extractor_v7.35.0", + "date": "Mon, 22 May 2023 06:34:32 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the TypeScript dependency to ~5.0.4" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.27.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.19`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.3.0`" + } + ] + } + }, + { + "version": "7.34.9", + "tag": "@microsoft/api-extractor_v7.34.9", + "date": "Fri, 12 May 2023 00:23:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.59.0`" + } + ] + } + }, + { + "version": "7.34.8", + "tag": "@microsoft/api-extractor_v7.34.8", + "date": "Thu, 04 May 2023 00:20:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.8`" + } + ] + } + }, + { + "version": "7.34.7", + "tag": "@microsoft/api-extractor_v7.34.7", + "date": "Mon, 01 May 2023 15:23:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.58.0`" + } + ] + } + }, + { + "version": "7.34.6", + "tag": "@microsoft/api-extractor_v7.34.6", + "date": "Sat, 29 Apr 2023 00:23:03 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.57.0`" + } + ] + } + }, + { + "version": "7.34.5", + "tag": "@microsoft/api-extractor_v7.34.5", + "date": "Thu, 27 Apr 2023 17:18:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.56.0`" + } + ] + } + }, + { + "version": "7.34.4", + "tag": "@microsoft/api-extractor_v7.34.4", + "date": "Fri, 10 Feb 2023 01:18:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.18`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.2.0`" + } + ] + } + }, + { + "version": "7.34.3", + "tag": "@microsoft/api-extractor_v7.34.3", + "date": "Sun, 05 Feb 2023 03:02:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.1`" + } + ] + } + }, + { + "version": "7.34.2", + "tag": "@microsoft/api-extractor_v7.34.2", + "date": "Wed, 01 Feb 2023 02:16:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.55.0`" + } + ] + } + }, + { + "version": "7.34.1", + "tag": "@microsoft/api-extractor_v7.34.1", + "date": "Mon, 30 Jan 2023 16:22:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.54.0`" + } + ] + } + }, + { + "version": "7.34.0", + "tag": "@microsoft/api-extractor_v7.34.0", + "date": "Wed, 25 Jan 2023 07:26:55 GMT", + "comments": { + "minor": [ + { + "comment": "Add new .api.json field `isAbstract` to track `abstract` modifier in ApiClass, ApiMethod, and ApiProperty via ApiAbstractMixin (GitHub #3661)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.26.0`" + } + ] + } + }, + { + "version": "7.33.8", + "tag": "@microsoft/api-extractor_v7.33.8", + "date": "Wed, 18 Jan 2023 22:44:12 GMT", + "comments": { + "patch": [ + { + "comment": "Use ts.getCheckFlags to fix TS 5.0" + } + ] + } + }, + { + "version": "7.33.7", + "tag": "@microsoft/api-extractor_v7.33.7", + "date": "Fri, 09 Dec 2022 16:18:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.25.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.3`" + } + ] + } + }, + { + "version": "7.33.6", + "tag": "@microsoft/api-extractor_v7.33.6", + "date": "Tue, 08 Nov 2022 01:20:55 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.13.1`" + } + ] + } + }, + { + "version": "7.33.5", + "tag": "@microsoft/api-extractor_v7.33.5", + "date": "Wed, 26 Oct 2022 00:16:16 GMT", + "comments": { + "patch": [ + { + "comment": "Update the @microsoft/tsdoc dependency version to 0.14.2." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.25.2`" + } + ] + } + }, + { + "version": "7.33.4", + "tag": "@microsoft/api-extractor_v7.33.4", + "date": "Mon, 17 Oct 2022 22:14:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.13.0`" + } + ] + } + }, + { + "version": "7.33.3", + "tag": "@microsoft/api-extractor_v7.33.3", + "date": "Mon, 17 Oct 2022 15:16:00 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a regression where the \"fileUrlPath\" property would contain a malformed path when API Extractor is run on Windows." + } + ] + } + }, + { + "version": "7.33.2", + "tag": "@microsoft/api-extractor_v7.33.2", + "date": "Fri, 14 Oct 2022 15:26:31 GMT", + "comments": { + "patch": [ + { + "comment": "Fix references from computed properties #3629" + } + ] + } + }, + { + "version": "7.33.1", + "tag": "@microsoft/api-extractor_v7.33.1", + "date": "Thu, 13 Oct 2022 00:20:15 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.25.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.2`" + } + ] + } + }, + { + "version": "7.33.0", + "tag": "@microsoft/api-extractor_v7.33.0", + "date": "Tue, 11 Oct 2022 23:49:12 GMT", + "comments": { + "minor": [ + { + "comment": "Extract the original source file path for relevant API items and add a new projectFolderUrl setting to the api-extractor.json config that allows one to specify what URL their project folder can be found at." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.25.0`" + } + ] + } + }, + { + "version": "7.32.1", + "tag": "@microsoft/api-extractor_v7.32.1", + "date": "Mon, 10 Oct 2022 15:23:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.24.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.17`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.1.1`" + } + ] + } + }, + { + "version": "7.32.0", + "tag": "@microsoft/api-extractor_v7.32.0", + "date": "Thu, 29 Sep 2022 07:13:06 GMT", + "comments": { + "minor": [ + { + "comment": "Update parser to TypeScript 4.8." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.24.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.53.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.16`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.1.0`" + } + ] + } + }, + { + "version": "7.31.2", + "tag": "@microsoft/api-extractor_v7.31.2", + "date": "Wed, 21 Sep 2022 20:21:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.24.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.52.0`" + } + ] + } + }, + { + "version": "7.31.1", + "tag": "@microsoft/api-extractor_v7.31.1", + "date": "Thu, 15 Sep 2022 00:18:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.24.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.15`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.0.1`" + } + ] + } + }, + { + "version": "7.31.0", + "tag": "@microsoft/api-extractor_v7.31.0", + "date": "Tue, 13 Sep 2022 00:16:55 GMT", + "comments": { + "minor": [ + { + "comment": "Fix an issue where aliased classes sometimes had incorrect canonical references in *.api.json (GitHub #3593)" + } + ] + } + }, + { + "version": "7.30.1", + "tag": "@microsoft/api-extractor_v7.30.1", + "date": "Mon, 12 Sep 2022 22:27:48 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a recent regression where items exported from both the entry point and from an exported namespace appeared only once in the API doc model (GitHub #3619)" + } + ] + } + }, + { + "version": "7.30.0", + "tag": "@microsoft/api-extractor_v7.30.0", + "date": "Fri, 02 Sep 2022 17:48:42 GMT", + "comments": { + "minor": [ + { + "comment": "Add new \"apiReport.includeForgottenExports\" and \"docModel.includeForgottenExports\" properties to control whether forgotten exports are included in the API report and doc model files." + }, + { + "comment": "Fix incorrect declaration references for symbols not exported from the package's entry point." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.24.0`" + } + ] + } + }, + { + "version": "7.29.5", + "tag": "@microsoft/api-extractor_v7.29.5", + "date": "Wed, 24 Aug 2022 03:01:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.23.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.1`" + } + ] + } + }, + { + "version": "7.29.4", + "tag": "@microsoft/api-extractor_v7.29.4", + "date": "Wed, 24 Aug 2022 00:14:38 GMT", + "comments": { + "patch": [ + { + "comment": "Remove use of LegacyAdapters.sortStable" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.23.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.51.0`" + } + ] + } + }, + { + "version": "7.29.3", + "tag": "@microsoft/api-extractor_v7.29.3", + "date": "Fri, 19 Aug 2022 00:17:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.23.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.2`" + } + ] + } + }, + { + "version": "7.29.2", + "tag": "@microsoft/api-extractor_v7.29.2", + "date": "Wed, 10 Aug 2022 09:52:12 GMT", + "comments": { + "patch": [ + { + "comment": "Fix incorrect declaration references for local symbols within namespaces" + } + ] + } + }, + { + "version": "7.29.1", + "tag": "@microsoft/api-extractor_v7.29.1", + "date": "Wed, 10 Aug 2022 08:12:16 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a regression where .api.json excerpts were sometimes missing tokens (GitHub #3561), and generally improve the quality of excerpt generation" + } + ] + } + }, + { + "version": "7.29.0", + "tag": "@microsoft/api-extractor_v7.29.0", + "date": "Wed, 03 Aug 2022 18:40:35 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade TypeScript dependency to 4.7" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.23.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.14`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `3.0.0`" + } + ] + } + }, + { + "version": "7.28.7", + "tag": "@microsoft/api-extractor_v7.28.7", + "date": "Mon, 01 Aug 2022 02:45:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.22.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.50.0`" + } + ] + } + }, + { + "version": "7.28.6", + "tag": "@microsoft/api-extractor_v7.28.6", + "date": "Thu, 21 Jul 2022 23:30:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.22.1`" + } + ] + } + }, + { + "version": "7.28.5", + "tag": "@microsoft/api-extractor_v7.28.5", + "date": "Thu, 21 Jul 2022 00:16:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.22.0`" + } + ] + } + }, + { + "version": "7.28.4", + "tag": "@microsoft/api-extractor_v7.28.4", + "date": "Fri, 08 Jul 2022 15:17:46 GMT", + "comments": { + "patch": [ + { + "comment": "Update api-extractor-template.json to \"testMode\" and \"enumMemberOrder\" comment sections." + } + ] + } + }, + { + "version": "7.28.3", + "tag": "@microsoft/api-extractor_v7.28.3", + "date": "Mon, 04 Jul 2022 15:15:13 GMT", + "comments": { + "patch": [ + { + "comment": "Make enumMemberOrder configuration field optional" + } + ] + } + }, + { + "version": "7.28.2", + "tag": "@microsoft/api-extractor_v7.28.2", + "date": "Thu, 30 Jun 2022 04:48:53 GMT", + "comments": { + "patch": [ + { + "comment": "Improve logic that determines whether an API item is readonly" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.21.0`" + } + ] + } + }, + { + "version": "7.28.1", + "tag": "@microsoft/api-extractor_v7.28.1", + "date": "Tue, 28 Jun 2022 22:47:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.20.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.49.0`" + } + ] + } + }, + { + "version": "7.28.0", + "tag": "@microsoft/api-extractor_v7.28.0", + "date": "Tue, 28 Jun 2022 00:23:32 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for the \"ignoreMissingEntryPoint\" ExtractorConfig option to allow for loading an ExtractorConfig before the target project is built." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.20.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.48.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.13`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.2`" + } + ] + } + }, + { + "version": "7.27.1", + "tag": "@microsoft/api-extractor_v7.27.1", + "date": "Mon, 27 Jun 2022 18:43:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.20.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.47.0`" + } + ] + } + }, + { + "version": "7.27.0", + "tag": "@microsoft/api-extractor_v7.27.0", + "date": "Sat, 25 Jun 2022 21:00:40 GMT", + "comments": { + "minor": [ + { + "comment": "API Extractor now populates an initializerTokenRange field for ApiProperty and ApiVariable items." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.20.0`" + } + ] + } + }, + { + "version": "7.26.1", + "tag": "@microsoft/api-extractor_v7.26.1", + "date": "Sat, 25 Jun 2022 01:54:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.19.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.46.0`" + } + ] + } + }, + { + "version": "7.26.0", + "tag": "@microsoft/api-extractor_v7.26.0", + "date": "Fri, 24 Jun 2022 07:16:47 GMT", + "comments": { + "minor": [ + { + "comment": "Include new configuration option for preserving enum member order" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.19.0`" + } + ] + } + }, + { + "version": "7.25.3", + "tag": "@microsoft/api-extractor_v7.25.3", + "date": "Thu, 23 Jun 2022 22:14:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.12.0`" + } + ] + } + }, + { + "version": "7.25.2", + "tag": "@microsoft/api-extractor_v7.25.2", + "date": "Fri, 17 Jun 2022 09:17:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.18.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.7`" + } + ] + } + }, + { + "version": "7.25.1", + "tag": "@microsoft/api-extractor_v7.25.1", + "date": "Fri, 17 Jun 2022 00:16:18 GMT", + "comments": { + "none": [ + { + "comment": "Fix a mistake in the config/api-extractor.json template's default." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.18.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.6`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.12`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.11.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.1`" + } + ] + } + }, + { + "version": "7.25.0", + "tag": "@microsoft/api-extractor_v7.25.0", + "date": "Tue, 07 Jun 2022 09:37:04 GMT", + "comments": { + "minor": [ + { + "comment": "Add an \"isReadonly\" field to the doc model to indicate whether a property or variable is readonly" + }, + { + "comment": "Add an \"isProtected\" field to the doc model to indicate protected class members" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.18.0`" + } + ] + } + }, + { + "version": "7.24.2", + "tag": "@microsoft/api-extractor_v7.24.2", + "date": "Wed, 25 May 2022 22:25:07 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where API Extractor would fail to run on a project where `\"moduleResolution\"` is set to `\"Node16\"` in `tsconfig.json`" + } + ] + } + }, + { + "version": "7.24.1", + "tag": "@microsoft/api-extractor_v7.24.1", + "date": "Thu, 19 May 2022 15:13:20 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a recent regression that produced an error \"Cannot read properties of undefined\" (GitHub #3423)" + } + ] + } + }, + { + "version": "7.24.0", + "tag": "@microsoft/api-extractor_v7.24.0", + "date": "Sat, 14 May 2022 03:01:27 GMT", + "comments": { + "minor": [ + { + "comment": "Throw an error early if API Extractor will attempt to process non-.d.ts files" + }, + { + "comment": "Generate API doc model nodes for setters without getters" + } + ], + "patch": [ + { + "comment": "Address edge case in excerptBuilder token range logic" + } + ] + } + }, + { + "version": "7.23.2", + "tag": "@microsoft/api-extractor_v7.23.2", + "date": "Tue, 10 May 2022 01:20:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.17.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.11.0`" + } + ] + } + }, + { + "version": "7.23.1", + "tag": "@microsoft/api-extractor_v7.23.1", + "date": "Wed, 04 May 2022 23:29:13 GMT", + "comments": { + "patch": [ + { + "comment": "Update the global variable analyzer to add support for changes to the TypeScript internals coming in v4.7" + } + ] + } + }, + { + "version": "7.23.0", + "tag": "@microsoft/api-extractor_v7.23.0", + "date": "Sat, 23 Apr 2022 02:13:06 GMT", + "comments": { + "minor": [ + { + "comment": "Update to TypeScript 4.6" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.17.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.4`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.11`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.10`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.6.0`" + } + ] + } + }, + { + "version": "7.22.2", + "tag": "@microsoft/api-extractor_v7.22.2", + "date": "Fri, 15 Apr 2022 00:12:36 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.17.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.10`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.9`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.4`" + } + ] + } + }, + { + "version": "7.22.1", + "tag": "@microsoft/api-extractor_v7.22.1", + "date": "Wed, 13 Apr 2022 15:12:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.17.0`" + } + ] + } + }, + { + "version": "7.22.0", + "tag": "@microsoft/api-extractor_v7.22.0", + "date": "Tue, 12 Apr 2022 23:29:34 GMT", + "comments": { + "minor": [ + { + "comment": "Add an alphaTrimmedFilePath option that adds support for generating a DTS rollup that inclues @alpha, @beta, and @public members." + } + ] + } + }, + { + "version": "7.21.3", + "tag": "@microsoft/api-extractor_v7.21.3", + "date": "Tue, 12 Apr 2022 02:58:32 GMT", + "comments": { + "patch": [ + { + "comment": "Update TSDoc dependencies." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.16.2`" + } + ] + } + }, + { + "version": "7.21.2", + "tag": "@microsoft/api-extractor_v7.21.2", + "date": "Sat, 09 Apr 2022 19:07:47 GMT", + "comments": { + "patch": [ + { + "comment": "Fix ambient modules bug caused by #3321." + } + ] + } + }, + { + "version": "7.21.1", + "tag": "@microsoft/api-extractor_v7.21.1", + "date": "Sat, 09 Apr 2022 02:24:26 GMT", + "comments": { + "patch": [ + { + "comment": "Rename the \"master\" branch to \"main\"." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.16.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.9`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.8`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.3`" + } + ] + } + }, + { + "version": "7.21.0", + "tag": "@microsoft/api-extractor_v7.21.0", + "date": "Fri, 08 Apr 2022 20:05:59 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for projects that use tsconfig.json \"baseUrl\" and \"paths\" settings to remap imports of local files (GitHub #3291)" + } + ] + } + }, + { + "version": "7.20.1", + "tag": "@microsoft/api-extractor_v7.20.1", + "date": "Wed, 06 Apr 2022 22:35:23 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where .api.json excerpt text included extra whitespace (GitHub #3316)" + } + ] + } + }, + { + "version": "7.20.0", + "tag": "@microsoft/api-extractor_v7.20.0", + "date": "Thu, 31 Mar 2022 02:06:05 GMT", + "comments": { + "minor": [ + { + "comment": "Updated api-extractor to extract whether a parameter is optional." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.16.0`" + } + ] + } + }, + { + "version": "7.19.5", + "tag": "@microsoft/api-extractor_v7.19.5", + "date": "Tue, 15 Mar 2022 19:15:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.15.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.8`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.2`" + } + ] + } + }, + { + "version": "7.19.4", + "tag": "@microsoft/api-extractor_v7.19.4", + "date": "Wed, 05 Jan 2022 16:07:47 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.15.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.45.0`" + } + ] + } + }, + { + "version": "7.19.3", + "tag": "@microsoft/api-extractor_v7.19.3", + "date": "Mon, 27 Dec 2021 16:10:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.15.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.7`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.1`" + } + ] + } + }, + { + "version": "7.19.2", + "tag": "@microsoft/api-extractor_v7.19.2", + "date": "Thu, 09 Dec 2021 20:34:41 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.15.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.2`" + } + ] + } + }, + { + "version": "7.19.1", + "tag": "@microsoft/api-extractor_v7.19.1", + "date": "Thu, 09 Dec 2021 00:21:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.15.0`" + } + ] + } + }, + { + "version": "7.19.0", + "tag": "@microsoft/api-extractor_v7.19.0", + "date": "Wed, 08 Dec 2021 16:14:05 GMT", + "comments": { + "minor": [ + { + "comment": "Update to TypeScript 4.5" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.14.0`" + } + ] + } + }, + { + "version": "7.18.21", + "tag": "@microsoft/api-extractor_v7.18.21", + "date": "Mon, 06 Dec 2021 16:08:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.18`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.5.0`" + } + ] + } + }, + { + "version": "7.18.20", + "tag": "@microsoft/api-extractor_v7.18.20", + "date": "Fri, 03 Dec 2021 03:05:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.17`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.44.0`" + } + ] + } + }, + { + "version": "7.18.19", + "tag": "@microsoft/api-extractor_v7.18.19", + "date": "Sat, 06 Nov 2021 00:09:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.16`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.2`" + } + ] + } + }, + { + "version": "7.18.18", + "tag": "@microsoft/api-extractor_v7.18.18", + "date": "Fri, 05 Nov 2021 15:09:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.15`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.5`" + } + ] + } + }, + { + "version": "7.18.17", + "tag": "@microsoft/api-extractor_v7.18.17", + "date": "Wed, 27 Oct 2021 00:08:15 GMT", + "comments": { + "patch": [ + { + "comment": "Update the package.json repository field to include the directory property." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.14`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.43.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.4`" + } + ] + } + }, + { + "version": "7.18.16", + "tag": "@microsoft/api-extractor_v7.18.16", + "date": "Wed, 13 Oct 2021 15:09:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.13`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.3`" + } + ] + } + }, + { + "version": "7.18.15", + "tag": "@microsoft/api-extractor_v7.18.15", + "date": "Fri, 08 Oct 2021 08:08:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.12`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.2`" + } + ] + } + }, + { + "version": "7.18.14", + "tag": "@microsoft/api-extractor_v7.18.14", + "date": "Thu, 07 Oct 2021 07:13:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.2`" + } + ] + } + }, + { + "version": "7.18.13", + "tag": "@microsoft/api-extractor_v7.18.13", + "date": "Tue, 05 Oct 2021 15:08:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.42.0`" + } + ] + } + }, + { + "version": "7.18.12", + "tag": "@microsoft/api-extractor_v7.18.12", + "date": "Mon, 04 Oct 2021 15:10:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.10.0`" + } + ] + } + }, + { + "version": "7.18.11", + "tag": "@microsoft/api-extractor_v7.18.11", + "date": "Fri, 24 Sep 2021 00:09:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.41.0`" + } + ] + } + }, + { + "version": "7.18.10", + "tag": "@microsoft/api-extractor_v7.18.10", + "date": "Thu, 23 Sep 2021 00:10:40 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade the `@types/node` dependency to version to version 12." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.9.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.1`" + } + ] + } + }, + { + "version": "7.18.9", + "tag": "@microsoft/api-extractor_v7.18.9", + "date": "Tue, 14 Sep 2021 01:17:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.2`" + } + ] + } + }, + { + "version": "7.18.8", + "tag": "@microsoft/api-extractor_v7.18.8", + "date": "Mon, 13 Sep 2021 15:07:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.1`" + } + ] + } + }, + { + "version": "7.18.7", + "tag": "@microsoft/api-extractor_v7.18.7", + "date": "Fri, 27 Aug 2021 00:07:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.3.0`" + } + ] + } + }, + { + "version": "7.18.6", + "tag": "@microsoft/api-extractor_v7.18.6", + "date": "Fri, 20 Aug 2021 15:08:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.9.0`" + } + ] + } + }, + { + "version": "7.18.5", + "tag": "@microsoft/api-extractor_v7.18.5", + "date": "Wed, 11 Aug 2021 00:07:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.40.0`" + } + ] + } + }, + { + "version": "7.18.4", + "tag": "@microsoft/api-extractor_v7.18.4", + "date": "Wed, 14 Jul 2021 15:06:29 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where the .d.ts rollup sometimes used \"default\" as an identifier name causing a syntax error (GitHub #2804)" + } + ] + } + }, + { + "version": "7.18.3", + "tag": "@microsoft/api-extractor_v7.18.3", + "date": "Tue, 13 Jul 2021 23:00:33 GMT", + "comments": { + "patch": [ + { + "comment": "Revert a workaround for TypeScript issue #44422 which was fixed in 4.3.3" + } + ] + } + }, + { + "version": "7.18.2", + "tag": "@microsoft/api-extractor_v7.18.2", + "date": "Mon, 12 Jul 2021 23:08:26 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.39.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.13`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.8.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.4.0`" + } + ] + } + }, + { + "version": "7.18.1", + "tag": "@microsoft/api-extractor_v7.18.1", + "date": "Thu, 08 Jul 2021 23:41:16 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a recent regression that reported \"Internal Error: indentDocComment cannot be nested\" (GitHub #2797)" + } + ] + } + }, + { + "version": "7.18.0", + "tag": "@microsoft/api-extractor_v7.18.0", + "date": "Thu, 08 Jul 2021 06:00:48 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for import() type expressions (GitHub #1050) -- Thank you @javier-garcia-meteologica and @adventure-yunfei for solving this difficult problem!" + }, + { + "comment": "Improve formatting of declarations in .d.ts rollup and .api.md files, fixing some indentation issues" + } + ] + } + }, + { + "version": "7.17.1", + "tag": "@microsoft/api-extractor_v7.17.1", + "date": "Thu, 01 Jul 2021 15:08:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.8.0`" + } + ] + } + }, + { + "version": "7.17.0", + "tag": "@microsoft/api-extractor_v7.17.0", + "date": "Wed, 30 Jun 2021 15:06:54 GMT", + "comments": { + "minor": [ + { + "comment": "Added support for \"import * as module from './local/module';\" (GitHub #1029) -- Big thanks to @adventure-yunfei, @mckn, @rbuckton, and @octogonz who all helped with this difficult PR!" + } + ], + "patch": [ + { + "comment": "Include /// directives in API report" + } + ] + } + }, + { + "version": "7.16.1", + "tag": "@microsoft/api-extractor_v7.16.1", + "date": "Fri, 04 Jun 2021 19:59:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.39.0`" + } + ] + } + }, + { + "version": "7.16.0", + "tag": "@microsoft/api-extractor_v7.16.0", + "date": "Fri, 04 Jun 2021 15:08:20 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 4.3" + } + ] + } + }, + { + "version": "7.15.2", + "tag": "@microsoft/api-extractor_v7.15.2", + "date": "Wed, 19 May 2021 00:11:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.38.0`" + } + ] + } + }, + { + "version": "7.15.1", + "tag": "@microsoft/api-extractor_v7.15.1", + "date": "Mon, 03 May 2021 15:10:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.37.0`" + } + ] + } + }, + { + "version": "7.15.0", + "tag": "@microsoft/api-extractor_v7.15.0", + "date": "Thu, 29 Apr 2021 23:26:50 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 4.2" + } + ] + } + }, + { + "version": "7.14.0", + "tag": "@microsoft/api-extractor_v7.14.0", + "date": "Tue, 20 Apr 2021 04:59:51 GMT", + "comments": { + "minor": [ + { + "comment": "Projects can now define custom tags using a tsdoc.json file" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.13.0`" + } + ] + } + }, + { + "version": "7.13.5", + "tag": "@microsoft/api-extractor_v7.13.5", + "date": "Mon, 12 Apr 2021 15:10:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.12`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.10`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.4`" + } + ] + } + }, + { + "version": "7.13.4", + "tag": "@microsoft/api-extractor_v7.13.4", + "date": "Thu, 08 Apr 2021 06:05:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.4`" + } + ] + } + }, + { + "version": "7.13.3", + "tag": "@microsoft/api-extractor_v7.13.3", + "date": "Tue, 06 Apr 2021 15:14:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.11`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.9`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.3`" + } + ] + } + }, + { + "version": "7.13.2", + "tag": "@microsoft/api-extractor_v7.13.2", + "date": "Thu, 04 Mar 2021 01:11:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.10`" + } + ] + } + }, + { + "version": "7.13.1", + "tag": "@microsoft/api-extractor_v7.13.1", + "date": "Fri, 05 Feb 2021 16:10:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.36.0`" + } + ] + } + }, + { + "version": "7.13.0", + "tag": "@microsoft/api-extractor_v7.13.0", + "date": "Wed, 13 Jan 2021 01:11:06 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 4.1" + } + ] + } + }, + { + "version": "7.12.1", + "tag": "@microsoft/api-extractor_v7.12.1", + "date": "Thu, 10 Dec 2020 23:25:49 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to TSDoc 0.12.24" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.9`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.8`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.2`" + } + ] + } + }, + { + "version": "7.12.0", + "tag": "@microsoft/api-extractor_v7.12.0", + "date": "Wed, 18 Nov 2020 08:19:54 GMT", + "comments": { + "minor": [ + { + "comment": "The \"isOptional\" .api.json field is now applied to both methods and properties" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.12.0`" + } + ] + } + }, + { + "version": "7.11.5", + "tag": "@microsoft/api-extractor_v7.11.5", + "date": "Wed, 18 Nov 2020 06:21:57 GMT", + "comments": { + "patch": [ + { + "comment": "Update .api.json file format to store a new field \"isOptional\" for documenting optional properties" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.11.0`" + } + ] + } + }, + { + "version": "7.11.4", + "tag": "@microsoft/api-extractor_v7.11.4", + "date": "Wed, 11 Nov 2020 01:08:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.8`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.1`" + } + ] + } + }, + { + "version": "7.11.3", + "tag": "@microsoft/api-extractor_v7.11.3", + "date": "Tue, 10 Nov 2020 23:13:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.35.0`" + } + ] + } + }, + { + "version": "7.11.2", + "tag": "@microsoft/api-extractor_v7.11.2", + "date": "Fri, 30 Oct 2020 06:38:38 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.7`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.7`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.3.0`" + } + ] + } + }, + { + "version": "7.11.1", + "tag": "@microsoft/api-extractor_v7.11.1", + "date": "Fri, 30 Oct 2020 00:10:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.6`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.3`" + } + ] + } + }, + { + "version": "7.11.0", + "tag": "@microsoft/api-extractor_v7.11.0", + "date": "Thu, 29 Oct 2020 06:14:19 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade the bundled compiler engine to TypeScript 4.0" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.6`" + } + ] + } + }, + { + "version": "7.10.6", + "tag": "@microsoft/api-extractor_v7.10.6", + "date": "Wed, 28 Oct 2020 01:18:03 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.5`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.2`" + } + ] + } + }, + { + "version": "7.10.5", + "tag": "@microsoft/api-extractor_v7.10.5", + "date": "Tue, 27 Oct 2020 15:10:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.4`" + } + ] + } + }, + { + "version": "7.10.4", + "tag": "@microsoft/api-extractor_v7.10.4", + "date": "Tue, 06 Oct 2020 00:24:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.3`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.1`" + } + ] + } + }, + { + "version": "7.10.3", + "tag": "@microsoft/api-extractor_v7.10.3", + "date": "Mon, 05 Oct 2020 22:36:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.2`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.2.0`" + } + ] + } + }, + { + "version": "7.10.2", + "tag": "@microsoft/api-extractor_v7.10.2", + "date": "Mon, 05 Oct 2020 15:10:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.2`" + } + ] + } + }, + { + "version": "7.10.1", + "tag": "@microsoft/api-extractor_v7.10.1", + "date": "Wed, 30 Sep 2020 18:39:17 GMT", + "comments": { + "patch": [ + { + "comment": "Update to build with @rushstack/heft-node-rig" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.1`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.3`" + } + ] + } + }, + { + "version": "7.10.0", + "tag": "@microsoft/api-extractor_v7.10.0", + "date": "Wed, 30 Sep 2020 06:53:53 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an InternalError reported when a declaration referred to itself using \"tyepof\"" + }, + { + "comment": "Update README.md" + } + ], + "minor": [ + { + "comment": "API Extractor now supports the config/rig.json system, as defined by @rushstack/rig-package" + }, + { + "comment": "Add IExtractorConfigPrepareOptions.projectFolderLookupToken" + }, + { + "comment": "Upgrade compiler; the API now requires TypeScript 3.9 or newer" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.10.0`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.34.0`" + }, + { + "comment": "Updating dependency \"@rushstack/rig-package\" to `0.2.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.7.0`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.2`" + } + ] + } + }, + { + "version": "7.9.22", + "tag": "@microsoft/api-extractor_v7.9.22", + "date": "Tue, 22 Sep 2020 05:45:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.10`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.1`" + } + ] + } + }, + { + "version": "7.9.21", + "tag": "@microsoft/api-extractor_v7.9.21", + "date": "Tue, 22 Sep 2020 01:45:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.9`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.1.0`" + } + ] + } + }, + { + "version": "7.9.20", + "tag": "@microsoft/api-extractor_v7.9.20", + "date": "Tue, 22 Sep 2020 00:08:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.8`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `2.0.0`" + } + ] + } + }, + { + "version": "7.9.19", + "tag": "@microsoft/api-extractor_v7.9.19", + "date": "Sat, 19 Sep 2020 04:37:26 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.2`" + } + ] + } + }, + { + "version": "7.9.18", + "tag": "@microsoft/api-extractor_v7.9.18", + "date": "Sat, 19 Sep 2020 03:33:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.1`" + } + ] + } + }, + { + "version": "7.9.17", + "tag": "@microsoft/api-extractor_v7.9.17", + "date": "Fri, 18 Sep 2020 22:57:24 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.4.0`" + } + ] + } + }, + { + "version": "7.9.16", + "tag": "@microsoft/api-extractor_v7.9.16", + "date": "Fri, 18 Sep 2020 21:49:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.33.0`" + } + ] + } + }, + { + "version": "7.9.15", + "tag": "@microsoft/api-extractor_v7.9.15", + "date": "Sun, 13 Sep 2020 01:53:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.9.0`" + } + ] + } + }, + { + "version": "7.9.14", + "tag": "@microsoft/api-extractor_v7.9.14", + "date": "Fri, 11 Sep 2020 02:13:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.22`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.32.0`" + } + ] + } + }, + { + "version": "7.9.13", + "tag": "@microsoft/api-extractor_v7.9.13", + "date": "Mon, 07 Sep 2020 07:37:37 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.21`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.31.0`" + } + ] + } + }, + { + "version": "7.9.12", + "tag": "@microsoft/api-extractor_v7.9.12", + "date": "Sat, 05 Sep 2020 18:56:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.20`" + } + ] + } + }, + { + "version": "7.9.11", + "tag": "@microsoft/api-extractor_v7.9.11", + "date": "Thu, 27 Aug 2020 11:27:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.19`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.30.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.3.0`" + } + ] + } + }, + { + "version": "7.9.10", + "tag": "@microsoft/api-extractor_v7.9.10", + "date": "Mon, 24 Aug 2020 07:35:20 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.18`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.29.1`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.2.1`" + } + ] + } + }, + { + "version": "7.9.9", + "tag": "@microsoft/api-extractor_v7.9.9", + "date": "Sat, 22 Aug 2020 05:55:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.17`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.29.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.2`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.2.0`" + } + ] + } + }, + { + "version": "7.9.8", + "tag": "@microsoft/api-extractor_v7.9.8", + "date": "Fri, 21 Aug 2020 01:21:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.1`" + } + ] + } + }, + { + "version": "7.9.7", + "tag": "@microsoft/api-extractor_v7.9.7", + "date": "Thu, 20 Aug 2020 15:13:53 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.6.0`" + } + ] + } + }, + { + "version": "7.9.6", + "tag": "@microsoft/api-extractor_v7.9.6", + "date": "Tue, 18 Aug 2020 23:59:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.16`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.28.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.5.0`" + } + ] + } + }, + { + "version": "7.9.5", + "tag": "@microsoft/api-extractor_v7.9.5", + "date": "Mon, 17 Aug 2020 04:53:23 GMT", + "comments": { + "none": [ + { + "comment": "Add keywords to package.json for discoverability" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.15`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.27.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.4.8`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.1.0`" + } + ] + } + }, + { + "version": "7.9.4", + "tag": "@microsoft/api-extractor_v7.9.4", + "date": "Wed, 12 Aug 2020 00:10:05 GMT", + "comments": { + "patch": [ + { + "comment": "Updated project to build with Heft" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.14`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.26.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" to `4.4.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" to `1.0.4`" + } + ] + } + }, + { + "version": "7.9.3", + "tag": "@microsoft/api-extractor_v7.9.3", + "date": "Wed, 05 Aug 2020 18:27:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" to `7.8.13`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" to `3.26.1`" + } + ] + } + }, + { + "version": "7.9.2", + "tag": "@microsoft/api-extractor_v7.9.2", + "date": "Thu, 09 Jul 2020 04:58:36 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue with handling of \"export { default } from 'package';\" (GitHub #2014)" + } + ] + } + }, + { + "version": "7.9.1", + "tag": "@microsoft/api-extractor_v7.9.1", + "date": "Fri, 03 Jul 2020 15:09:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.11` to `7.8.12`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.4` to `3.25.0`" + } + ] + } + }, + { + "version": "7.9.0", + "tag": "@microsoft/api-extractor_v7.9.0", + "date": "Fri, 03 Jul 2020 05:46:41 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where chained compiler errors were not formatted correctly" + }, + { + "comment": "Log the TypeScript bundled compiler version, and warn if it is outdated" + } + ], + "minor": [ + { + "comment": "Add support for ECMAScript private fields (new in TypeScript 3.8)" + }, + { + "comment": "Add support for \"import type\" imports (new in TypeScript 3.8)" + }, + { + "comment": "Upgrade the bundled compiler engine to TypeScript 3.9" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.5` to `4.4.6`" + } + ] + } + }, + { + "version": "7.8.15", + "tag": "@microsoft/api-extractor_v7.8.15", + "date": "Thu, 25 Jun 2020 06:43:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.10` to `7.8.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.3` to `3.24.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.4` to `4.4.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `1.0.1` to `1.0.2`" + } + ] + } + }, + { + "version": "7.8.14", + "tag": "@microsoft/api-extractor_v7.8.14", + "date": "Wed, 24 Jun 2020 09:50:48 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.9` to `7.8.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.2` to `3.24.3`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.3` to `4.4.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `1.0.0` to `1.0.1`" + } + ] + } + }, + { + "version": "7.8.13", + "tag": "@microsoft/api-extractor_v7.8.13", + "date": "Wed, 24 Jun 2020 09:04:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.8` to `7.8.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.1` to `3.24.2`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.2` to `4.4.3`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.8` to `1.0.0`" + } + ] + } + }, + { + "version": "7.8.12", + "tag": "@microsoft/api-extractor_v7.8.12", + "date": "Mon, 15 Jun 2020 22:17:17 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where documentation hyperlinks were sometimes missing when using the \"bundledPackages\" feature (GitHub #1933)" + } + ] + } + }, + { + "version": "7.8.11", + "tag": "@microsoft/api-extractor_v7.8.11", + "date": "Wed, 10 Jun 2020 20:48:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.7` to `7.8.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.24.0` to `3.24.1`" + } + ] + } + }, + { + "version": "7.8.10", + "tag": "@microsoft/api-extractor_v7.8.10", + "date": "Mon, 01 Jun 2020 08:34:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.1` to `4.4.2`" + } + ] + } + }, + { + "version": "7.8.9", + "tag": "@microsoft/api-extractor_v7.8.9", + "date": "Sat, 30 May 2020 02:59:54 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.6` to `7.8.7`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.23.1` to `3.24.0`" + } + ] + } + }, + { + "version": "7.8.8", + "tag": "@microsoft/api-extractor_v7.8.8", + "date": "Thu, 28 May 2020 05:59:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.5` to `7.8.6`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.23.0` to `3.23.1`" + } + ] + } + }, + { + "version": "7.8.7", + "tag": "@microsoft/api-extractor_v7.8.7", + "date": "Wed, 27 May 2020 05:15:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.4` to `7.8.5`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.22.1` to `3.23.0`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.4.0` to `4.4.1`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.7` to `0.5.8`" + } + ] + } + }, + { + "version": "7.8.6", + "tag": "@microsoft/api-extractor_v7.8.6", + "date": "Tue, 26 May 2020 23:00:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.3` to `7.8.4`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.22.0` to `3.22.1`" + } + ] + } + }, + { + "version": "7.8.5", + "tag": "@microsoft/api-extractor_v7.8.5", + "date": "Fri, 22 May 2020 15:08:42 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.2` to `7.8.3`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.21.0` to `3.22.0`" + } + ] + } + }, + { + "version": "7.8.4", + "tag": "@microsoft/api-extractor_v7.8.4", + "date": "Thu, 21 May 2020 23:09:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.1` to `7.8.2`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.20.0` to `3.21.0`" + } + ] + } + }, + { + "version": "7.8.3", + "tag": "@microsoft/api-extractor_v7.8.3", + "date": "Thu, 21 May 2020 15:41:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.8.0` to `7.8.1`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.7` to `3.20.0`" + } + ] + } + }, + { + "version": "7.8.2", + "tag": "@microsoft/api-extractor_v7.8.2", + "date": "Tue, 19 May 2020 15:08:19 GMT", + "comments": { + "patch": [ + { + "comment": "Report an error to indicate that \"import()\" types are not supported" + } + ] + } + }, + { + "version": "7.8.1", + "tag": "@microsoft/api-extractor_v7.8.1", + "date": "Fri, 15 May 2020 08:10:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.3.14` to `4.4.0`" + } + ] + } + }, + { + "version": "7.8.0", + "tag": "@microsoft/api-extractor_v7.8.0", + "date": "Wed, 06 May 2020 08:23:45 GMT", + "comments": { + "minor": [ + { + "comment": "Version update only" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.11` to `7.8.0`" + } + ] + } + }, + { + "version": "7.7.13", + "tag": "@microsoft/api-extractor_v7.7.13", + "date": "Wed, 08 Apr 2020 04:07:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.10` to `7.7.11`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.6` to `3.19.7`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.3.13` to `4.3.14`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.6` to `0.5.7`" + } + ] + } + }, + { + "version": "7.7.12", + "tag": "@microsoft/api-extractor_v7.7.12", + "date": "Sun, 29 Mar 2020 00:04:12 GMT", + "comments": { + "patch": [ + { + "comment": "Improve analysis of types exposed via global variables (fixes GitHub issues #1765, #1095, and #1316)" + } + ] + } + }, + { + "version": "7.7.11", + "tag": "@microsoft/api-extractor_v7.7.11", + "date": "Sat, 28 Mar 2020 00:37:16 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to TSdoc 0.12.19 to fix an issue where `

` wasn't allowed as an HTML tag in a doc comment" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.9` to `7.7.10`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.5` to `3.19.6`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.3.12` to `4.3.13`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.5` to `0.5.6`" + } + ] + } + }, + { + "version": "7.7.10", + "tag": "@microsoft/api-extractor_v7.7.10", + "date": "Wed, 18 Mar 2020 15:07:47 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade cyclic dependencies" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.8` to `7.7.9`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.4` to `3.19.5`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.3.11` to `4.3.12`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.4` to `0.5.5`" + } + ] + } + }, + { + "version": "7.7.9", + "tag": "@microsoft/api-extractor_v7.7.9", + "date": "Tue, 17 Mar 2020 23:55:58 GMT", + "comments": { + "patch": [ + { + "comment": "Replace dependencies whose NPM scope was renamed from `@microsoft` to `@rushstack`" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.7` to `7.7.8`" + }, + { + "comment": "Updating dependency \"@rushstack/node-core-library\" from `3.19.3` to `3.19.4`" + }, + { + "comment": "Updating dependency \"@rushstack/ts-command-line\" from `4.3.10` to `4.3.11`" + } + ] + } + }, + { + "version": "7.7.8", + "tag": "@microsoft/api-extractor_v7.7.8", + "date": "Tue, 28 Jan 2020 02:23:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.6` to `7.7.7`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.2` to `3.19.3`" + } + ] + } + }, + { + "version": "7.7.7", + "tag": "@microsoft/api-extractor_v7.7.7", + "date": "Thu, 23 Jan 2020 01:07:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.5` to `7.7.6`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.1` to `3.19.2`" + } + ] + } + }, + { + "version": "7.7.6", + "tag": "@microsoft/api-extractor_v7.7.6", + "date": "Tue, 21 Jan 2020 21:56:13 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.4` to `7.7.5`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.19.0` to `3.19.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.9` to `4.3.10`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.3` to `0.5.4`" + } + ] + } + }, + { + "version": "7.7.5", + "tag": "@microsoft/api-extractor_v7.7.5", + "date": "Sun, 19 Jan 2020 02:26:52 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade Node typings to Node 10" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.3` to `7.7.4`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.3` to `3.19.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.8` to `4.3.9`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.2` to `0.5.3`" + } + ] + } + }, + { + "version": "7.7.4", + "tag": "@microsoft/api-extractor_v7.7.4", + "date": "Fri, 17 Jan 2020 01:08:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.2` to `7.7.3`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.2` to `3.18.3`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.7` to `4.3.8`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.1` to `0.5.2`" + } + ] + } + }, + { + "version": "7.7.3", + "tag": "@microsoft/api-extractor_v7.7.3", + "date": "Tue, 14 Jan 2020 01:34:15 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where \"ae-incompatible-release-tags\" was sometimes reported incorectly for property setters (GitHub #1681)" + } + ] + } + }, + { + "version": "7.7.2", + "tag": "@microsoft/api-extractor_v7.7.2", + "date": "Thu, 09 Jan 2020 06:44:12 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an error \"Cannot read property 'externalModuleIndicator' of undefined\" (GitHub #1652)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.1` to `7.7.2`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.1` to `3.18.2`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.6` to `4.3.7`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.5.0` to `0.5.1`" + } + ] + } + }, + { + "version": "7.7.1", + "tag": "@microsoft/api-extractor_v7.7.1", + "date": "Wed, 08 Jan 2020 00:11:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.7.0` to `7.7.1`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.18.0` to `3.18.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.5` to `4.3.6`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.2` to `0.5.0`" + } + ] + } + }, + { + "version": "7.7.0", + "tag": "@microsoft/api-extractor_v7.7.0", + "date": "Tue, 03 Dec 2019 03:17:43 GMT", + "comments": { + "minor": [ + { + "comment": "Improve declaration reference syntax to allow linking to overloaded functions/methods" + }, + { + "comment": "Fix an issue with TypeScript 3.7, which now emits separate signatures for property getters/setters" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.6.0` to `7.7.0`" + } + ] + } + }, + { + "version": "7.6.2", + "tag": "@microsoft/api-extractor_v7.6.2", + "date": "Sun, 24 Nov 2019 00:54:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.6` to `7.6.0`" + } + ] + } + }, + { + "version": "7.6.1", + "tag": "@microsoft/api-extractor_v7.6.1", + "date": "Wed, 20 Nov 2019 06:14:28 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where the newlineKind setting wasn't being applied correctly" + } + ] + } + }, + { + "version": "7.6.0", + "tag": "@microsoft/api-extractor_v7.6.0", + "date": "Fri, 15 Nov 2019 04:50:50 GMT", + "comments": { + "minor": [ + { + "comment": "Make newline type for generated files configurable" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.5` to `7.5.6`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.17.1` to `3.18.0`" + } + ] + } + }, + { + "version": "7.5.6", + "tag": "@microsoft/api-extractor_v7.5.6", + "date": "Mon, 11 Nov 2019 16:07:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.4` to `7.5.5`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.17.0` to `3.17.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.4` to `4.3.5`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.1` to `0.4.2`" + } + ] + } + }, + { + "version": "7.5.5", + "tag": "@microsoft/api-extractor_v7.5.5", + "date": "Wed, 06 Nov 2019 22:44:18 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for TypeScript 3.7" + } + ] + } + }, + { + "version": "7.5.4", + "tag": "@microsoft/api-extractor_v7.5.4", + "date": "Tue, 05 Nov 2019 06:49:28 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where API reports sometimes were ordered differently depending on the version of NodeJS (GitHub #1552)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.3` to `7.5.4`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.16.0` to `3.17.0`" + } + ] + } + }, + { + "version": "7.5.3", + "tag": "@microsoft/api-extractor_v7.5.3", + "date": "Tue, 05 Nov 2019 01:08:39 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.2` to `7.5.3`" + } + ] + } + }, + { + "version": "7.5.2", + "tag": "@microsoft/api-extractor_v7.5.2", + "date": "Tue, 22 Oct 2019 06:24:44 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.1` to `7.5.2`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.15.1` to `3.16.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.3` to `4.3.4`" + }, + { + "comment": "Updating dependency \"@rushstack/eslint-config\" from `0.4.0` to `0.4.1`" + } + ] + } + }, + { + "version": "7.5.1", + "tag": "@microsoft/api-extractor_v7.5.1", + "date": "Fri, 18 Oct 2019 15:15:01 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.2` to `4.3.3`" + } + ] + } + }, + { + "version": "7.5.0", + "tag": "@microsoft/api-extractor_v7.5.0", + "date": "Sun, 06 Oct 2019 00:27:39 GMT", + "comments": { + "minor": [ + { + "comment": "Allow separate release tags for overloaded functions and methods" + }, + { + "comment": "Add new api-extractor.json config setting \"bundledPackages\"" + } + ] + } + }, + { + "version": "7.4.7", + "tag": "@microsoft/api-extractor_v7.4.7", + "date": "Fri, 04 Oct 2019 00:15:22 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where IExtractorConfigPrepareOptions.packageJson was ignored (GitHub #1559)" + } + ] + } + }, + { + "version": "7.4.6", + "tag": "@microsoft/api-extractor_v7.4.6", + "date": "Sun, 29 Sep 2019 23:56:29 GMT", + "comments": { + "patch": [ + { + "comment": "Update repository URL" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.5.0` to `7.5.1`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.15.0` to `3.15.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.1` to `4.3.2`" + } + ] + } + }, + { + "version": "7.4.5", + "tag": "@microsoft/api-extractor_v7.4.5", + "date": "Wed, 25 Sep 2019 15:15:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.4.2` to `7.5.0`" + } + ] + } + }, + { + "version": "7.4.4", + "tag": "@microsoft/api-extractor_v7.4.4", + "date": "Tue, 24 Sep 2019 02:58:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.3.0` to `4.3.1`" + } + ] + } + }, + { + "version": "7.4.3", + "tag": "@microsoft/api-extractor_v7.4.3", + "date": "Mon, 23 Sep 2019 15:14:55 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.4.1` to `7.4.2`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.2` to `3.15.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.8` to `4.3.0`" + } + ] + } + }, + { + "version": "7.4.2", + "tag": "@microsoft/api-extractor_v7.4.2", + "date": "Wed, 11 Sep 2019 19:56:23 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for an exported name that conflicts with a global name (GitHub #1350)" + } + ] + } + }, + { + "version": "7.4.1", + "tag": "@microsoft/api-extractor_v7.4.1", + "date": "Tue, 10 Sep 2019 22:32:23 GMT", + "comments": { + "patch": [ + { + "comment": "Update documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.4.0` to `7.4.1`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.1` to `3.14.2`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.7` to `4.2.8`" + } + ] + } + }, + { + "version": "7.4.0", + "tag": "@microsoft/api-extractor_v7.4.0", + "date": "Tue, 10 Sep 2019 20:38:33 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for generating declaration references" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.3.4` to `7.4.0`" + } + ] + } + }, + { + "version": "7.3.11", + "tag": "@microsoft/api-extractor_v7.3.11", + "date": "Wed, 04 Sep 2019 18:28:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.3.3` to `7.3.4`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.14.0` to `3.14.1`" + } + ] + } + }, + { + "version": "7.3.10", + "tag": "@microsoft/api-extractor_v7.3.10", + "date": "Wed, 04 Sep 2019 15:15:37 GMT", + "comments": { + "patch": [ + { + "comment": "Update TSDoc dependency to 0.12.14" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.3.2` to `7.3.3`" + } + ] + } + }, + { + "version": "7.3.9", + "tag": "@microsoft/api-extractor_v7.3.9", + "date": "Fri, 30 Aug 2019 00:14:32 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a problem where Unicode API names were not handled correctly" + } + ] + } + }, + { + "version": "7.3.8", + "tag": "@microsoft/api-extractor_v7.3.8", + "date": "Mon, 12 Aug 2019 15:15:14 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.6` to `4.2.7`" + } + ] + } + }, + { + "version": "7.3.7", + "tag": "@microsoft/api-extractor_v7.3.7", + "date": "Thu, 08 Aug 2019 15:14:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.3.1` to `7.3.2`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.13.0` to `3.14.0`" + } + ] + } + }, + { + "version": "7.3.6", + "tag": "@microsoft/api-extractor_v7.3.6", + "date": "Thu, 08 Aug 2019 00:49:05 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where a function with only one declaration was assigned an overloadIndex of 0 instead of 1" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.3.0` to `7.3.1`" + } + ] + } + }, + { + "version": "7.3.5", + "tag": "@microsoft/api-extractor_v7.3.5", + "date": "Mon, 05 Aug 2019 22:04:32 GMT", + "comments": { + "patch": [ + { + "comment": "Security updates." + } + ] + } + }, + { + "version": "7.3.4", + "tag": "@microsoft/api-extractor_v7.3.4", + "date": "Tue, 23 Jul 2019 01:13:01 GMT", + "comments": { + "patch": [ + { + "comment": "ApiItem.name is now quoted when it contains invalid identifier characters, to avoid conflicts with an ECMAScript symbol expression" + } + ] + } + }, + { + "version": "7.3.3", + "tag": "@microsoft/api-extractor_v7.3.3", + "date": "Mon, 22 Jul 2019 19:13:10 GMT", + "comments": { + "patch": [ + { + "comment": "Update to use new api-extractor-model" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.2.0` to `7.3.0`" + } + ] + } + }, + { + "version": "7.3.2", + "tag": "@microsoft/api-extractor_v7.3.2", + "date": "Fri, 12 Jul 2019 19:12:46 GMT", + "comments": { + "patch": [ + { + "comment": "Clarify docs for \"--typescript-compiler-folder\"" + } + ] + } + }, + { + "version": "7.3.1", + "tag": "@microsoft/api-extractor_v7.3.1", + "date": "Thu, 11 Jul 2019 19:13:08 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for TypeScript 3.5" + } + ] + } + }, + { + "version": "7.3.0", + "tag": "@microsoft/api-extractor_v7.3.0", + "date": "Tue, 09 Jul 2019 19:13:24 GMT", + "comments": { + "minor": [ + { + "comment": "Add a \"--diagnostics\" command-line option to help when troubleshooting problems" + } + ] + } + }, + { + "version": "7.2.3", + "tag": "@microsoft/api-extractor_v7.2.3", + "date": "Mon, 08 Jul 2019 19:12:18 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a problem when analyzing .d.ts files that appear in the same folder as the corresponding .ts file (GitHub #1310)" + } + ] + } + }, + { + "version": "7.2.2", + "tag": "@microsoft/api-extractor_v7.2.2", + "date": "Sat, 29 Jun 2019 02:30:10 GMT", + "comments": { + "patch": [ + { + "comment": "Fix GitHub issue #1304 where \"IExtractorInvokeOptions.typescriptCompilerFolder\" did not work with TypeScript 3.4" + } + ] + } + }, + { + "version": "7.2.1", + "tag": "@microsoft/api-extractor_v7.2.1", + "date": "Wed, 12 Jun 2019 19:12:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.5` to `4.2.6`" + } + ] + } + }, + { + "version": "7.2.0", + "tag": "@microsoft/api-extractor_v7.2.0", + "date": "Tue, 11 Jun 2019 00:48:06 GMT", + "comments": { + "minor": [ + { + "comment": "Generate ApiTypeParameter entries and type alias types" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.1.3` to `7.2.0`" + } + ] + } + }, + { + "version": "7.1.8", + "tag": "@microsoft/api-extractor_v7.1.8", + "date": "Wed, 05 Jun 2019 19:12:34 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where TSDoc index selectors (ApiParameterListMixin.overloadIndex) started from 0, whereas TSDoc requires a nonzero number" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.1.2` to `7.1.3`" + } + ] + } + }, + { + "version": "7.1.7", + "tag": "@microsoft/api-extractor_v7.1.7", + "date": "Tue, 04 Jun 2019 05:51:53 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade api-extractor-model to remove ApiConstructor.isStatic, since TypeScript constructors cannot be static" + }, + { + "comment": "Improve handling of symbolic property and method names." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.1.1` to `7.1.2`" + } + ] + } + }, + { + "version": "7.1.6", + "tag": "@microsoft/api-extractor_v7.1.6", + "date": "Mon, 27 May 2019 04:13:44 GMT", + "comments": { + "patch": [ + { + "comment": "Fix incorrect path resolution for the \"extends\" field when loading tsconfig.json" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.1.0` to `7.1.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.4` to `4.2.5`" + } + ] + } + }, + { + "version": "7.1.5", + "tag": "@microsoft/api-extractor_v7.1.5", + "date": "Mon, 13 May 2019 02:08:35 GMT", + "comments": { + "patch": [ + { + "comment": "Broaden support for default imports" + } + ] + } + }, + { + "version": "7.1.4", + "tag": "@microsoft/api-extractor_v7.1.4", + "date": "Mon, 06 May 2019 20:46:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.3` to `4.2.4`" + } + ] + } + }, + { + "version": "7.1.3", + "tag": "@microsoft/api-extractor_v7.1.3", + "date": "Mon, 06 May 2019 19:34:54 GMT", + "comments": { + "patch": [ + { + "comment": "Add a new setting \"omitTrimmingComments\" to prevent extra comments from being emitted in the .d.ts rollup" + } + ] + } + }, + { + "version": "7.1.2", + "tag": "@microsoft/api-extractor_v7.1.2", + "date": "Mon, 06 May 2019 19:11:16 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where ExtractorResult.warningCount was not incremented for messages handled by IExtractorInvokeOptions.messageCallback (GitHub #1258)" + } + ] + } + }, + { + "version": "7.1.1", + "tag": "@microsoft/api-extractor_v7.1.1", + "date": "Tue, 30 Apr 2019 23:08:02 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where API signatures were sometimes truncated in the .api.json file (GitHub #1249)" + } + ] + } + }, + { + "version": "7.1.0", + "tag": "@microsoft/api-extractor_v7.1.0", + "date": "Tue, 16 Apr 2019 11:01:37 GMT", + "comments": { + "minor": [ + { + "comment": "Initial stable release of API Extractor 7" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.28` to `7.1.0`" + } + ] + } + }, + { + "version": "7.0.42", + "tag": "@microsoft/api-extractor_v7.0.42", + "date": "Fri, 12 Apr 2019 06:13:16 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a regression that prevented certain types of warnings from being reported" + } + ] + } + }, + { + "version": "7.0.41", + "tag": "@microsoft/api-extractor_v7.0.41", + "date": "Thu, 11 Apr 2019 07:14:01 GMT", + "comments": { + "patch": [ + { + "comment": "THIS IS A RELEASE CANDIDATE FOR API-EXTRACTOR 7" + }, + { + "comment": "(Breaking change) Rename \"mainEntryPointFile\" to \"mainEntryPointFilePath\" so all settings use a consistent naming convention" + }, + { + "comment": "(Breaking change) Paths that appear in api-extractor.json are now resolved relative to the config file unless prefixed with the `` token" + }, + { + "comment": "Add a new api-extractor.json setting \"tsconfigFilePath\" for customizing the tsconfig.json path" + }, + { + "comment": "Replace ExtractorConfig.packageJsonFullPath with ExtractorConfig.packageFolder" + }, + { + "comment": "Upgrade API Extractor to use TypeScript 3.4 for analysis" + } + ] + } + }, + { + "version": "7.0.40", + "tag": "@microsoft/api-extractor_v7.0.40", + "date": "Tue, 09 Apr 2019 05:31:01 GMT", + "comments": { + "patch": [ + { + "comment": "Improve the \"--local\" option to automatically create the API report file if it is missing" + } + ] + } + }, + { + "version": "7.0.39", + "tag": "@microsoft/api-extractor_v7.0.39", + "date": "Mon, 08 Apr 2019 19:12:52 GMT", + "comments": { + "patch": [ + { + "comment": "Rename \"addToApiReviewFile\" setting to \"addToApiReportFile\"" + } + ] + } + }, + { + "version": "7.0.38", + "tag": "@microsoft/api-extractor_v7.0.38", + "date": "Sat, 06 Apr 2019 02:05:51 GMT", + "comments": { + "patch": [ + { + "comment": "(Breaking change) Removed the ILogger API and renamed ExtractorMessageLogLevel to ExtractorLogLevel" + }, + { + "comment": "(Breaking change) Extractor console output is now modeled as ExtractorMessage objects and can be customized/filtered/handled by IExtractorInvokeOptions.messageCallback" + } + ] + } + }, + { + "version": "7.0.37", + "tag": "@microsoft/api-extractor_v7.0.37", + "date": "Fri, 05 Apr 2019 04:16:16 GMT", + "comments": { + "patch": [ + { + "comment": "Introduce \"api-extractor init\" command-line that helps enable API Extractor for a new project" + }, + { + "comment": "(Breaking change) Major redesign of the API used to invoke API Extractor" + }, + { + "comment": "(Breaking change) Major redesign of the api-extractor.json config file format" + }, + { + "comment": "Add a CompilerState API that allows an optimization where multiple invocations of Extractor can reuse the same TypeScript compiler analysis" + } + ] + } + }, + { + "version": "7.0.36", + "tag": "@microsoft/api-extractor_v7.0.36", + "date": "Wed, 03 Apr 2019 02:58:33 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where .d.ts.map file were sometimes mapped to the wrong location" + } + ] + } + }, + { + "version": "7.0.35", + "tag": "@microsoft/api-extractor_v7.0.35", + "date": "Sat, 30 Mar 2019 22:27:16 GMT", + "comments": { + "patch": [ + { + "comment": "Reintroduce the generated documentation notice for internal constructors" + }, + { + "comment": "Add limited support for resolving @inheritDoc references to external packages by postprocessing them in api-documenter" + } + ] + } + }, + { + "version": "7.0.34", + "tag": "@microsoft/api-extractor_v7.0.34", + "date": "Thu, 28 Mar 2019 19:14:27 GMT", + "comments": { + "patch": [ + { + "comment": "Validate `@link` tags and report a warning if the link cannot be resolved" + } + ] + } + }, + { + "version": "7.0.33", + "tag": "@microsoft/api-extractor_v7.0.33", + "date": "Tue, 26 Mar 2019 20:54:18 GMT", + "comments": { + "patch": [ + { + "comment": "Reintroduce support for `@inheritDoc` tags" + } + ] + } + }, + { + "version": "7.0.32", + "tag": "@microsoft/api-extractor_v7.0.32", + "date": "Sat, 23 Mar 2019 03:48:31 GMT", + "comments": { + "patch": [ + { + "comment": "If the TSDoc summary is missing for a class constructor, then automatically generate it" + }, + { + "comment": "Reintroduce support for the `@preapproved` TSDoc tag" + } + ] + } + }, + { + "version": "7.0.31", + "tag": "@microsoft/api-extractor_v7.0.31", + "date": "Thu, 21 Mar 2019 04:59:11 GMT", + "comments": { + "patch": [ + { + "comment": "Reintroduce \"ae-internal-missing-underscore\" warning for API items marked as `@internal` but whose name does not start with an underscore" + } + ] + } + }, + { + "version": "7.0.30", + "tag": "@microsoft/api-extractor_v7.0.30", + "date": "Thu, 21 Mar 2019 01:15:32 GMT", + "comments": { + "patch": [ + { + "comment": "Improve the API review file generation to include imports and support multiple exports" + } + ] + } + }, + { + "version": "7.0.29", + "tag": "@microsoft/api-extractor_v7.0.29", + "date": "Wed, 20 Mar 2019 19:14:49 GMT", + "comments": { + "patch": [ + { + "comment": "API Extractor can now analyze packages whose package.json file is missing the \"version\" field" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.27` to `7.0.28`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.12.1` to `3.13.0`" + } + ] + } + }, + { + "version": "7.0.28", + "tag": "@microsoft/api-extractor_v7.0.28", + "date": "Mon, 18 Mar 2019 04:28:43 GMT", + "comments": { + "patch": [ + { + "comment": "Rename the \"ae-inconsistent-release-tags\" warning to \"ae-different-release-tags\"" + }, + { + "comment": "Introduce a new warning \"ae-incompatible-release-tags\" that checks for API signatures that reference types with incompatible release tags" + }, + { + "comment": "Fix an issue where this error was sometimes reported incorrectly: \"The messages.extractorMessageReporting table contains an unrecognized identifier ___\"" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.26` to `7.0.27`" + }, + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.12.0` to `3.12.1`" + } + ] + } + }, + { + "version": "7.0.27", + "tag": "@microsoft/api-extractor_v7.0.27", + "date": "Fri, 15 Mar 2019 19:13:25 GMT", + "comments": { + "patch": [ + { + "comment": "(Breaking change) The file extension for API review files has changed from \".api.ts\" to \"api.md\". For details see https://github.com/microsoft/web-build-tools/issues/1123" + } + ] + } + }, + { + "version": "7.0.26", + "tag": "@microsoft/api-extractor_v7.0.26", + "date": "Wed, 13 Mar 2019 19:13:14 GMT", + "comments": { + "patch": [ + { + "comment": "Refactor code to move the IndentedWriter API from api-extractor-model to api-documenter" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.25` to `7.0.26`" + } + ] + } + }, + { + "version": "7.0.25", + "tag": "@microsoft/api-extractor_v7.0.25", + "date": "Wed, 13 Mar 2019 01:14:05 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade TSDoc" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.24` to `7.0.25`" + } + ] + } + }, + { + "version": "7.0.24", + "tag": "@microsoft/api-extractor_v7.0.24", + "date": "Mon, 11 Mar 2019 16:13:36 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where spurious TSDoc warnings were issued because the TSDoc parser was configured improperly" + }, + { + "comment": "Move the .api.json related APIs into a new NPM package @microsoft/api-extractor-model" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/api-extractor-model\" from `7.0.23` to `7.0.24`" + } + ] + } + }, + { + "version": "7.0.23", + "tag": "@microsoft/api-extractor_v7.0.23", + "date": "Tue, 05 Mar 2019 17:13:11 GMT", + "comments": { + "patch": [ + { + "comment": "Issue a warning when an exported type refers to another local type that is not exported (ae-forgotten-export)" + }, + { + "comment": "The export analyzer now correctly handles symbols imported using \"import x = require('y');\" notation" + } + ] + } + }, + { + "version": "7.0.22", + "tag": "@microsoft/api-extractor_v7.0.22", + "date": "Mon, 04 Mar 2019 17:13:19 GMT", + "comments": { + "patch": [ + { + "comment": "Every error/warning message reported by API Extractor now has an associated message identifier" + }, + { + "comment": "Add a new section to api-extractor.json for configuring how errors get reported, with ability to suppress individual errors" + }, + { + "comment": "Reintroduce the ability to report issues by writing warnings into the API review file" + }, + { + "comment": "Fix an issue where members of type literals were incorrectly being flagged as \"(undocumented)\"" + }, + { + "comment": "Error messages now cite the original .ts source file, if a source map is present. (To enable this, specify `\"declarationMap\": true` in tsconfig.json.)" + } + ] + } + }, + { + "version": "7.0.21", + "tag": "@microsoft/api-extractor_v7.0.21", + "date": "Wed, 27 Feb 2019 22:13:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.11.1` to `3.12.0`" + } + ] + } + }, + { + "version": "7.0.20", + "tag": "@microsoft/api-extractor_v7.0.20", + "date": "Wed, 27 Feb 2019 17:13:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.11.0` to `3.11.1`" + } + ] + } + }, + { + "version": "7.0.19", + "tag": "@microsoft/api-extractor_v7.0.19", + "date": "Mon, 18 Feb 2019 17:13:23 GMT", + "comments": { + "minor": [ + { + "comment": "New way to resolve & generate TSDoc metadata file" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.10.0` to `3.11.0`" + } + ] + } + }, + { + "version": "7.0.18", + "tag": "@microsoft/api-extractor_v7.0.18", + "date": "Tue, 12 Feb 2019 17:13:12 GMT", + "comments": { + "patch": [ + { + "comment": "Add a workaround for the issue where .d.ts rollups sometimes define names that conflict with a global symbol (the full solution is tracked by GitHub #1095)" + } + ] + } + }, + { + "version": "7.0.17", + "tag": "@microsoft/api-extractor_v7.0.17", + "date": "Mon, 11 Feb 2019 10:32:37 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where API Extractor neglected to analyze \"typeof\" expressions" + }, + { + "comment": "Fix an issue where declarations inside a namespace were sometimes being incorrectly emitted as top-level exports of the .d.ts rollup" + } + ] + } + }, + { + "version": "7.0.16", + "tag": "@microsoft/api-extractor_v7.0.16", + "date": "Mon, 11 Feb 2019 08:55:57 GMT", + "comments": { + "patch": [ + { + "comment": "Redesign the analyzer so that when an external symbol is reexported by the working package, the local object (AstImport) and external object (AstSymbol) are kept separate" + }, + { + "comment": "Fix a number of bugs where external symbols were misinterpreted as being part of the local project" + }, + { + "comment": "Eliminate a number of errors involving unusual language constructs, by avoiding analysis of external symbols unless it's really necessary" + }, + { + "comment": "Simplify the AstSymbol.nominalAnalysis concept and associated code" + }, + { + "comment": "Improve .d.ts rollup trimming to handle reexported symbols correctly" + } + ] + } + }, + { + "version": "7.0.15", + "tag": "@microsoft/api-extractor_v7.0.15", + "date": "Mon, 11 Feb 2019 03:31:55 GMT", + "comments": { + "patch": [ + { + "comment": "The `--debug` parameter now automatically breaks in the debugger when InternalError is thrown" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.9.0` to `3.10.0`" + } + ] + } + }, + { + "version": "7.0.14", + "tag": "@microsoft/api-extractor_v7.0.14", + "date": "Thu, 31 Jan 2019 17:03:49 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to TSDoc 0.12.5, which allows `$` in `@param` names" + }, + { + "comment": "Add \"testMode\" option in api-extractor.json to eliminate spurious diffs in test files when the version number gets bumped" + }, + { + "comment": "Normalize newlines for excerpt strings in the .api.json file" + } + ] + } + }, + { + "version": "7.0.13", + "tag": "@microsoft/api-extractor_v7.0.13", + "date": "Sat, 19 Jan 2019 03:47:47 GMT", + "comments": { + "patch": [ + { + "comment": "Move the skipLibCheck into the config file." + } + ] + } + }, + { + "version": "7.0.12", + "tag": "@microsoft/api-extractor_v7.0.12", + "date": "Sat, 19 Jan 2019 01:17:51 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where files using \"export=\" were incorrectly interpreted as having ambient declarations" + } + ] + } + }, + { + "version": "7.0.11", + "tag": "@microsoft/api-extractor_v7.0.11", + "date": "Fri, 18 Jan 2019 00:52:21 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for circular references between files that use `export * from \"____\";`" + } + ] + } + }, + { + "version": "7.0.10", + "tag": "@microsoft/api-extractor_v7.0.10", + "date": "Thu, 17 Jan 2019 00:37:54 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for exports of the form `export * from \"____\";`" + }, + { + "comment": "Improve the analyzer to allow a declaration to be exported more than once" + }, + { + "comment": "Fix inconsistent newlines in .api.ts files" + } + ] + } + }, + { + "version": "7.0.9", + "tag": "@microsoft/api-extractor_v7.0.9", + "date": "Thu, 10 Jan 2019 01:57:52 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue with rolling up default exports (https://github.com/microsoft/web-build-tools/issues/1007)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.8.3` to `3.9.0`" + } + ] + } + }, + { + "version": "7.0.8", + "tag": "@microsoft/api-extractor_v7.0.8", + "date": "Thu, 20 Dec 2018 17:04:08 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where it was possible to import forgotten declarations from a .d.ts rollup, even though they did not have an explicit \"export\" modifier" + } + ] + } + }, + { + "version": "7.0.7", + "tag": "@microsoft/api-extractor_v7.0.7", + "date": "Wed, 19 Dec 2018 05:57:33 GMT", + "comments": { + "patch": [ + { + "comment": "Extend ApiModel to support new item kinds: ApiCallSignature, ApiConstructor, ApiConstructSignature, ApiFunction, ApiIndexSignature, ApiTypeAlias, and ApiVariable" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.8.2` to `3.8.3`" + } + ] + } + }, + { + "version": "7.0.6", + "tag": "@microsoft/api-extractor_v7.0.6", + "date": "Fri, 14 Dec 2018 19:43:46 GMT", + "comments": { + "patch": [ + { + "comment": "Update web site URLs" + } + ] + } + }, + { + "version": "7.0.5", + "tag": "@microsoft/api-extractor_v7.0.5", + "date": "Thu, 13 Dec 2018 02:58:10 GMT", + "comments": { + "patch": [ + { + "comment": "Remove unused jju dependency" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.8.1` to `3.8.2`" + } + ] + } + }, + { + "version": "7.0.4", + "tag": "@microsoft/api-extractor_v7.0.4", + "date": "Wed, 12 Dec 2018 17:04:19 GMT", + "comments": { + "patch": [ + { + "comment": "Reintroduce support for \"extends\" and \"implements\" heritage clauses" + }, + { + "comment": "Redesign the Excerpt API to support multiple subranges (e.g. for a list of \"implements\" clauses)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.8.0` to `3.8.1`" + } + ] + } + }, + { + "version": "7.0.3", + "tag": "@microsoft/api-extractor_v7.0.3", + "date": "Fri, 07 Dec 2018 17:04:56 GMT", + "comments": { + "patch": [ + { + "comment": "Added more API documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.7.1` to `3.8.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.2` to `4.2.3`" + } + ] + } + }, + { + "version": "7.0.2", + "tag": "@microsoft/api-extractor_v7.0.2", + "date": "Wed, 05 Dec 2018 19:57:03 GMT", + "comments": { + "patch": [ + { + "comment": "fix reexported types from an external package for dts rollup" + } + ] + } + }, + { + "version": "7.0.1", + "tag": "@microsoft/api-extractor_v7.0.1", + "date": "Wed, 05 Dec 2018 17:04:18 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where .d.ts trimming did not properly handle variable declarations (GitHub #976)" + } + ] + } + }, + { + "version": "7.0.0", + "tag": "@microsoft/api-extractor_v7.0.0", + "date": "Thu, 29 Nov 2018 07:02:09 GMT", + "comments": { + "major": [ + { + "comment": "THIS IS A BETA RELEASE - We are bumping the version to \"7.0.0\" to simplify dogfooding. This release is not yet ready for general usage." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.7.0` to `3.7.1`" + } + ] + } + }, + { + "version": "6.3.0", + "tag": "@microsoft/api-extractor_v6.3.0", + "date": "Wed, 28 Nov 2018 19:29:53 GMT", + "comments": { + "minor": [ + { + "comment": "Support \"extends\" field in api-extractor.json config files for easier management of monorepos with many projects" + } + ] + } + }, + { + "version": "6.2.0", + "tag": "@microsoft/api-extractor_v6.2.0", + "date": "Wed, 28 Nov 2018 02:17:11 GMT", + "comments": { + "minor": [ + { + "comment": "Introduce a new build output \"dist/tsdoc-metdata.json\", which completely replaces the old \"tsdocFlavor\" field in package.json" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.6.0` to `3.7.0`" + } + ] + } + }, + { + "version": "6.1.6", + "tag": "@microsoft/api-extractor_v6.1.6", + "date": "Fri, 16 Nov 2018 21:37:10 GMT", + "comments": { + "patch": [ + { + "comment": "Add support for emitting `/// ` directives in .d.ts rollups (GitHub issue #946)" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.5.2` to `3.6.0`" + } + ] + } + }, + { + "version": "6.1.5", + "tag": "@microsoft/api-extractor_v6.1.5", + "date": "Fri, 16 Nov 2018 00:59:00 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where .d.ts trimming did not work for exported variable declarations (GitHub #936)" + } + ] + } + }, + { + "version": "6.1.4", + "tag": "@microsoft/api-extractor_v6.1.4", + "date": "Fri, 09 Nov 2018 23:07:39 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to TSDoc 0.21.2, which improves trimming of link text in `@link` tags" + } + ] + } + }, + { + "version": "6.1.3", + "tag": "@microsoft/api-extractor_v6.1.3", + "date": "Wed, 07 Nov 2018 21:04:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.5.1` to `3.5.2`" + } + ] + } + }, + { + "version": "6.1.2", + "tag": "@microsoft/api-extractor_v6.1.2", + "date": "Mon, 05 Nov 2018 17:04:24 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to @microsoft/tsdoc 0.12.0" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.5.0` to `3.5.1`" + } + ] + } + }, + { + "version": "6.1.1", + "tag": "@microsoft/api-extractor_v6.1.1", + "date": "Thu, 01 Nov 2018 19:32:52 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where EcmaScript symbols (\"computed property names\") were missing from .d.ts rollups" + } + ] + } + }, + { + "version": "6.1.0", + "tag": "@microsoft/api-extractor_v6.1.0", + "date": "Wed, 31 Oct 2018 17:00:54 GMT", + "comments": { + "minor": [ + { + "comment": "Added an api to invoke api extractor processor by supplying api extractor json config file." + } + ] + } + }, + { + "version": "6.0.9", + "tag": "@microsoft/api-extractor_v6.0.9", + "date": "Thu, 25 Oct 2018 23:20:40 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.4.0` to `3.5.0`" + } + ] + } + }, + { + "version": "6.0.8", + "tag": "@microsoft/api-extractor_v6.0.8", + "date": "Thu, 25 Oct 2018 08:56:02 GMT", + "comments": { + "patch": [ + { + "comment": "Fix issue where `DocErrorText.text` returned `[object Object]` instead of the text " + } + ] + } + }, + { + "version": "6.0.7", + "tag": "@microsoft/api-extractor_v6.0.7", + "date": "Wed, 24 Oct 2018 16:03:10 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.3.1` to `3.4.0`" + } + ] + } + }, + { + "version": "6.0.6", + "tag": "@microsoft/api-extractor_v6.0.6", + "date": "Thu, 18 Oct 2018 01:32:20 GMT", + "comments": { + "patch": [ + { + "comment": "Fix isAbsolute check for mainDtsRollupPath" + } + ] + } + }, + { + "version": "6.0.5", + "tag": "@microsoft/api-extractor_v6.0.5", + "date": "Wed, 17 Oct 2018 21:04:49 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.3.0` to `3.3.1`" + } + ] + } + }, + { + "version": "6.0.4", + "tag": "@microsoft/api-extractor_v6.0.4", + "date": "Wed, 17 Oct 2018 14:43:24 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a regression where namespaces were sometimes incorrectly handled in \"conservative\" mode" + }, + { + "comment": "Update the command line to look for api-extractor.json in both the \"./config\" folder and the project folder" + }, + { + "comment": "Allow type references in namespaces when namespaceSupport=conservative" + } + ] + } + }, + { + "version": "6.0.3", + "tag": "@microsoft/api-extractor_v6.0.3", + "date": "Thu, 11 Oct 2018 23:26:07 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where `import x from \".\"` was sometimes not processed correctly" + } + ] + } + }, + { + "version": "6.0.2", + "tag": "@microsoft/api-extractor_v6.0.2", + "date": "Tue, 09 Oct 2018 06:58:01 GMT", + "comments": { + "patch": [ + { + "comment": "Fix a regression where API Extractor was sometimes reporting incorrect line numbers" + } + ] + } + }, + { + "version": "6.0.1", + "tag": "@microsoft/api-extractor_v6.0.1", + "date": "Mon, 08 Oct 2018 16:04:27 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.2.0` to `3.3.0`" + } + ] + } + }, + { + "version": "6.0.0", + "tag": "@microsoft/api-extractor_v6.0.0", + "date": "Sun, 07 Oct 2018 06:15:56 GMT", + "comments": { + "major": [ + { + "comment": "(Breaking change) API Extractor 6 introduces support for TSDoc doc comment syntax! Please see https://api-extractor.com/ for documentation. To learn more about the TSDoc standard, check out https://github.com/microsoft/tsdoc" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.1.0` to `3.2.0`" + } + ] + } + }, + { + "version": "5.13.1", + "tag": "@microsoft/api-extractor_v5.13.1", + "date": "Fri, 28 Sep 2018 16:05:35 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.0.1` to `3.1.0`" + } + ] + } + }, + { + "version": "5.13.0", + "tag": "@microsoft/api-extractor_v5.13.0", + "date": "Wed, 26 Sep 2018 21:39:40 GMT", + "comments": { + "minor": [ + { + "comment": "Add new command line option --skip-lib-check" + } + ] + } + }, + { + "version": "5.12.2", + "tag": "@microsoft/api-extractor_v5.12.2", + "date": "Mon, 24 Sep 2018 23:06:40 GMT", + "comments": { + "patch": [ + { + "comment": "Allow doc comments to use TSDoc's \"@defaultvalue\" tag (but the value is not yet passed to the documentation pipeline)" + } + ] + } + }, + { + "version": "5.12.1", + "tag": "@microsoft/api-extractor_v5.12.1", + "date": "Fri, 21 Sep 2018 16:04:42 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where TypeScript errors are often logged as \"[Object object]\" instead of the actual error message." + } + ] + } + }, + { + "version": "5.12.0", + "tag": "@microsoft/api-extractor_v5.12.0", + "date": "Thu, 20 Sep 2018 23:57:21 GMT", + "comments": { + "minor": [ + { + "comment": "Add new feature: Support using a different version of the TypeScript compiler." + } + ] + } + }, + { + "version": "5.11.2", + "tag": "@microsoft/api-extractor_v5.11.2", + "date": "Tue, 18 Sep 2018 21:04:55 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where parameters mentioned in comments were attempting to be analyzed by api-extractor." + } + ] + } + }, + { + "version": "5.11.1", + "tag": "@microsoft/api-extractor_v5.11.1", + "date": "Thu, 06 Sep 2018 01:25:25 GMT", + "comments": { + "patch": [ + { + "comment": "Update \"repository\" field in package.json" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `3.0.0` to `3.0.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.1` to `4.2.2`" + } + ] + } + }, + { + "version": "5.11.0", + "tag": "@microsoft/api-extractor_v5.11.0", + "date": "Mon, 03 Sep 2018 16:04:45 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade api-extractor to internally use TypeScript 3.0." + } + ] + } + }, + { + "version": "5.10.8", + "tag": "@microsoft/api-extractor_v5.10.8", + "date": "Wed, 29 Aug 2018 06:36:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `2.2.1` to `3.0.0`" + } + ] + } + }, + { + "version": "5.10.7", + "tag": "@microsoft/api-extractor_v5.10.7", + "date": "Thu, 23 Aug 2018 18:18:53 GMT", + "comments": { + "patch": [ + { + "comment": "Republish all packages in web-build-tools to resolve GitHub issue #782" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `2.2.0` to `2.2.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.2.0` to `4.2.1`" + } + ] + } + }, + { + "version": "5.10.6", + "tag": "@microsoft/api-extractor_v5.10.6", + "date": "Wed, 22 Aug 2018 20:58:58 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `2.1.1` to `2.2.0`" + } + ] + } + }, + { + "version": "5.10.5", + "tag": "@microsoft/api-extractor_v5.10.5", + "date": "Wed, 22 Aug 2018 16:03:25 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `2.1.0` to `2.1.1`" + } + ] + } + }, + { + "version": "5.10.4", + "tag": "@microsoft/api-extractor_v5.10.4", + "date": "Tue, 21 Aug 2018 16:04:38 GMT", + "comments": { + "patch": [ + { + "comment": "fix namespace name for export statement`" + } + ] + } + }, + { + "version": "5.10.3", + "tag": "@microsoft/api-extractor_v5.10.3", + "date": "Thu, 09 Aug 2018 21:03:22 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `2.0.0` to `2.1.0`" + } + ] + } + }, + { + "version": "5.10.2", + "tag": "@microsoft/api-extractor_v5.10.2", + "date": "Thu, 09 Aug 2018 16:04:24 GMT", + "comments": { + "patch": [ + { + "comment": "Update lodash." + } + ] + } + }, + { + "version": "5.10.1", + "tag": "@microsoft/api-extractor_v5.10.1", + "date": "Thu, 26 Jul 2018 16:04:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.5.0` to `2.0.0`" + } + ] + } + }, + { + "version": "5.10.0", + "tag": "@microsoft/api-extractor_v5.10.0", + "date": "Tue, 17 Jul 2018 16:02:52 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for new \"@eventproperty\" AEDoc tag, which indicates that a class/interface property should be documented as an event" + } + ] + } + }, + { + "version": "5.9.1", + "tag": "@microsoft/api-extractor_v5.9.1", + "date": "Tue, 03 Jul 2018 21:03:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.4.1` to `1.5.0`" + } + ] + } + }, + { + "version": "5.9.0", + "tag": "@microsoft/api-extractor_v5.9.0", + "date": "Sat, 23 Jun 2018 02:21:20 GMT", + "comments": { + "minor": [ + { + "comment": "Add new IMarkupHtmlTag API" + }, + { + "comment": "AEDoc now allows HTML tags inside doc comments, which can be disabled using a backslash escape" + } + ] + } + }, + { + "version": "5.8.1", + "tag": "@microsoft/api-extractor_v5.8.1", + "date": "Thu, 21 Jun 2018 08:27:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.4.0` to `1.4.1`" + } + ] + } + }, + { + "version": "5.8.0", + "tag": "@microsoft/api-extractor_v5.8.0", + "date": "Tue, 19 Jun 2018 19:35:11 GMT", + "comments": { + "minor": [ + { + "comment": "For namespaceSupport=permissive, allow arbitrary nesting of namespaces" + } + ], + "patch": [ + { + "comment": "Fix an issue where multi-line type literals sometimes had inconsistent newlines in the *.api.json file" + } + ] + } + }, + { + "version": "5.7.3", + "tag": "@microsoft/api-extractor_v5.7.3", + "date": "Fri, 08 Jun 2018 08:43:52 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.3.2` to `1.4.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.1.0` to `4.2.0`" + } + ] + } + }, + { + "version": "5.7.2", + "tag": "@microsoft/api-extractor_v5.7.2", + "date": "Thu, 31 May 2018 01:39:33 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.3.1` to `1.3.2`" + } + ] + } + }, + { + "version": "5.7.1", + "tag": "@microsoft/api-extractor_v5.7.1", + "date": "Tue, 15 May 2018 02:26:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.3.0` to `1.3.1`" + } + ] + } + }, + { + "version": "5.7.0", + "tag": "@microsoft/api-extractor_v5.7.0", + "date": "Tue, 15 May 2018 00:18:10 GMT", + "comments": { + "minor": [ + { + "comment": "Add support for new AEDoc tags @sealed, @virtual, and @override" + } + ] + } + }, + { + "version": "5.6.8", + "tag": "@microsoft/api-extractor_v5.6.8", + "date": "Fri, 04 May 2018 00:42:38 GMT", + "comments": { + "patch": [ + { + "comment": "Fix the formatting of a log message." + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.2.0` to `1.3.0`" + } + ] + } + }, + { + "version": "5.6.7", + "tag": "@microsoft/api-extractor_v5.6.7", + "date": "Tue, 01 May 2018 22:03:20 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where the *.d.ts rollup trimming did not trim import statements" + } + ] + } + }, + { + "version": "5.6.6", + "tag": "@microsoft/api-extractor_v5.6.6", + "date": "Fri, 27 Apr 2018 03:04:32 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `4.0.0` to `4.1.0`" + } + ] + } + }, + { + "version": "5.6.5", + "tag": "@microsoft/api-extractor_v5.6.5", + "date": "Thu, 19 Apr 2018 21:25:56 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.1.1` to `4.0.0`" + } + ] + } + }, + { + "version": "5.6.4", + "tag": "@microsoft/api-extractor_v5.6.4", + "date": "Thu, 19 Apr 2018 17:02:06 GMT", + "comments": { + "patch": [ + { + "comment": "Fix errors in schema documentation" + } + ] + } + }, + { + "version": "5.6.3", + "tag": "@microsoft/api-extractor_v5.6.3", + "date": "Tue, 03 Apr 2018 16:05:29 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.1.0` to `1.2.0`" + } + ] + } + }, + { + "version": "5.6.2", + "tag": "@microsoft/api-extractor_v5.6.2", + "date": "Mon, 02 Apr 2018 16:05:24 GMT", + "comments": { + "patch": [ + { + "comment": "Refactor to use new @microsoft/node-core-library" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `1.0.0` to `1.1.0`" + } + ] + } + }, + { + "version": "5.6.1", + "tag": "@microsoft/api-extractor_v5.6.1", + "date": "Tue, 27 Mar 2018 01:34:25 GMT", + "comments": { + "patch": [ + { + "comment": "Update build config so API Extractor builds using the latest version of itself" + } + ] + } + }, + { + "version": "5.6.0", + "tag": "@microsoft/api-extractor_v5.6.0", + "date": "Sun, 25 Mar 2018 01:26:19 GMT", + "comments": { + "patch": [ + { + "comment": "In preparation for initial release, the \"Package Typings\" feature was renamed to \"DTS Rollup\"" + }, + { + "comment": "Fix an issue where the @packagedocumentation comment was sometimes getting mixed into the middle of the rollup *.d.ts file" + } + ], + "minor": [ + { + "comment": "Improve the api-extractor.json config file so that *.d.ts rollups go in separate folders, and trimming can now be disabled" + } + ] + } + }, + { + "version": "5.5.2", + "tag": "@microsoft/api-extractor_v5.5.2", + "date": "Fri, 23 Mar 2018 00:34:53 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade colors to version ~1.2.1" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.1.0` to `3.1.1`" + } + ] + } + }, + { + "version": "5.5.1", + "tag": "@microsoft/api-extractor_v5.5.1", + "date": "Tue, 20 Mar 2018 02:44:45 GMT", + "comments": { + "patch": [ + { + "comment": "Improve packageTypings generator to trim nested members according to their release tag" + }, + { + "comment": "Fix a bug where packageTypings failed to handle merged declarations properly" + } + ] + } + }, + { + "version": "5.5.0", + "tag": "@microsoft/api-extractor_v5.5.0", + "date": "Sat, 17 Mar 2018 02:54:22 GMT", + "comments": { + "minor": [ + { + "comment": "Overhaul the packageTypings generator analysis to get ready for the upcoming nested member trimming" + }, + { + "comment": "Breaking change: Any projects using the package typings feature must now have a \"tsdoc\" section in their package.json" + } + ], + "patch": [ + { + "comment": "Add \"--debug\" flag for debugging" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.8.0` to `1.0.0`" + } + ] + } + }, + { + "version": "5.4.0", + "tag": "@microsoft/api-extractor_v5.4.0", + "date": "Thu, 15 Mar 2018 20:00:50 GMT", + "comments": { + "minor": [ + { + "comment": "Add a new setting validationRules.missingReleaseTags to optionally remove the requirement that every API item should have a release tag" + }, + { + "comment": "Add new API \"Markup.formatApiItemReference()\"" + } + ], + "patch": [ + { + "comment": "Fix an issue where the automatically generated documentation for class constructors sometimes had a broken hyperlink" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.7` to `3.1.0`" + } + ] + } + }, + { + "version": "5.3.9", + "tag": "@microsoft/api-extractor_v5.3.9", + "date": "Thu, 15 Mar 2018 16:05:43 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.7.3` to `0.8.0`" + } + ] + } + }, + { + "version": "5.3.8", + "tag": "@microsoft/api-extractor_v5.3.8", + "date": "Mon, 12 Mar 2018 20:36:19 GMT", + "comments": { + "patch": [ + { + "comment": "Locked down some \"@types/\" dependency versions to avoid upgrade conflicts" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.6` to `3.0.7`" + } + ] + } + }, + { + "version": "5.3.7", + "tag": "@microsoft/api-extractor_v5.3.7", + "date": "Tue, 06 Mar 2018 17:04:51 GMT", + "comments": { + "patch": [ + { + "comment": "Add preliminary support for preview and public outputs for packageTypings generator" + } + ] + } + }, + { + "version": "5.3.6", + "tag": "@microsoft/api-extractor_v5.3.6", + "date": "Fri, 02 Mar 2018 01:13:59 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.7.2` to `0.7.3`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.5` to `3.0.6`" + } + ] + } + }, + { + "version": "5.3.5", + "tag": "@microsoft/api-extractor_v5.3.5", + "date": "Tue, 27 Feb 2018 22:05:57 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.7.1` to `0.7.2`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.4` to `3.0.5`" + } + ] + } + }, + { + "version": "5.3.4", + "tag": "@microsoft/api-extractor_v5.3.4", + "date": "Wed, 21 Feb 2018 22:04:19 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.7.0` to `0.7.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.3` to `3.0.4`" + } + ] + } + }, + { + "version": "5.3.3", + "tag": "@microsoft/api-extractor_v5.3.3", + "date": "Wed, 21 Feb 2018 03:13:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.6.1` to `0.7.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.2` to `3.0.3`" + } + ] + } + }, + { + "version": "5.3.2", + "tag": "@microsoft/api-extractor_v5.3.2", + "date": "Sat, 17 Feb 2018 02:53:49 GMT", + "comments": { + "patch": [ + { + "comment": "Fix several bugs with the way that imports were being deduplicated by the packageTypings feature" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.6.0` to `0.6.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.1` to `3.0.2`" + } + ] + } + }, + { + "version": "5.3.1", + "tag": "@microsoft/api-extractor_v5.3.1", + "date": "Fri, 16 Feb 2018 22:05:23 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.5.1` to `0.6.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `3.0.0` to `3.0.1`" + } + ] + } + }, + { + "version": "5.3.0", + "tag": "@microsoft/api-extractor_v5.3.0", + "date": "Fri, 16 Feb 2018 17:05:11 GMT", + "comments": { + "patch": [ + { + "comment": "Fix an issue where the packageTypings feature sometimes emitted \"default\" instead of the class name" + }, + { + "comment": "Improve the packageTypings feature to support triple-slash references to typings" + } + ], + "minor": [ + { + "comment": "Fix an issue where the packageTypings feature didn't handle some import/export patterns" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.5.0` to `0.5.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.10` to `3.0.0`" + } + ] + } + }, + { + "version": "5.2.7", + "tag": "@microsoft/api-extractor_v5.2.7", + "date": "Wed, 07 Feb 2018 17:05:11 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.10` to `0.5.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.9` to `2.3.10`" + } + ] + } + }, + { + "version": "5.2.6", + "tag": "@microsoft/api-extractor_v5.2.6", + "date": "Fri, 26 Jan 2018 22:05:30 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.9` to `0.4.10`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.8` to `2.3.9`" + } + ] + } + }, + { + "version": "5.2.5", + "tag": "@microsoft/api-extractor_v5.2.5", + "date": "Fri, 26 Jan 2018 17:53:38 GMT", + "comments": { + "patch": [ + { + "comment": "Force a patch bump in case the previous version was an empty package" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.8` to `0.4.9`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.7` to `2.3.8`" + } + ] + } + }, + { + "version": "5.2.4", + "tag": "@microsoft/api-extractor_v5.2.4", + "date": "Fri, 26 Jan 2018 00:36:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.7` to `0.4.8`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.6` to `2.3.7`" + } + ] + } + }, + { + "version": "5.2.3", + "tag": "@microsoft/api-extractor_v5.2.3", + "date": "Tue, 23 Jan 2018 17:05:28 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.6` to `0.4.7`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.5` to `2.3.6`" + } + ] + } + }, + { + "version": "5.2.2", + "tag": "@microsoft/api-extractor_v5.2.2", + "date": "Thu, 18 Jan 2018 03:23:46 GMT", + "comments": { + "patch": [ + { + "comment": "Enable package typings generated by api-extractor" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.5` to `0.4.6`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.4` to `2.3.5`" + } + ] + } + }, + { + "version": "5.2.1", + "tag": "@microsoft/api-extractor_v5.2.1", + "date": "Thu, 18 Jan 2018 00:48:06 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.4` to `0.4.5`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.3` to `2.3.4`" + } + ] + } + }, + { + "version": "5.2.0", + "tag": "@microsoft/api-extractor_v5.2.0", + "date": "Thu, 18 Jan 2018 00:27:23 GMT", + "comments": { + "minor": [ + { + "comment": "Improve the packageTypings feature to support abstract classes and \"import * as X\" imports" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.3` to `0.4.4`" + } + ] + } + }, + { + "version": "5.1.3", + "tag": "@microsoft/api-extractor_v5.1.3", + "date": "Wed, 17 Jan 2018 10:49:31 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.2` to `0.4.3`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.2` to `2.3.3`" + } + ] + } + }, + { + "version": "5.1.2", + "tag": "@microsoft/api-extractor_v5.1.2", + "date": "Fri, 12 Jan 2018 03:35:22 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "4589070d63c8c1d5d77bfa5e52b4f9fd2734e19f", + "comment": "Add some incremental improvements for the experimental PackageTypingsGenerator feature" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.1` to `0.4.2`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.1` to `2.3.2`" + } + ] + } + }, + { + "version": "5.1.1", + "tag": "@microsoft/api-extractor_v5.1.1", + "date": "Thu, 11 Jan 2018 22:31:51 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.4.0` to `0.4.1`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.3.0` to `2.3.1`" + } + ] + } + }, + { + "version": "5.1.0", + "tag": "@microsoft/api-extractor_v5.1.0", + "date": "Wed, 10 Jan 2018 20:40:01 GMT", + "comments": { + "minor": [ + { + "author": "Nicholas Pape ", + "commit": "1271a0dc21fedb882e7953f491771724f80323a1", + "comment": "Upgrade to Node 8" + } + ], + "patch": [ + { + "author": "pgonzal ", + "commit": "e6afac460ac96fa5e7a54d6ccab84aca4546cc5e", + "comment": "Continued progress for the experimental PackageTypingsGenerator" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.26` to `0.4.0`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.14` to `2.3.0`" + } + ] + } + }, + { + "version": "5.0.1", + "tag": "@microsoft/api-extractor_v5.0.1", + "date": "Tue, 09 Jan 2018 17:05:51 GMT", + "comments": { + "patch": [ + { + "author": "Nicholas Pape ", + "commit": "d00b6549d13610fbb6f84be3478b532be9da0747", + "comment": "Get web-build-tools building with pnpm" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.25` to `0.3.26`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.13` to `2.2.14`" + } + ] + } + }, + { + "version": "5.0.0", + "tag": "@microsoft/api-extractor_v5.0.0", + "date": "Sun, 07 Jan 2018 05:12:08 GMT", + "comments": { + "major": [ + { + "author": "pgonzal ", + "commit": "aa0c67382d4dee0cde40ee84a581dbdcdabe77ef", + "comment": "API Extractor now processes *.d.ts files instead of *.ts files" + } + ], + "minor": [ + { + "author": "pgonzal ", + "commit": "aa0c67382d4dee0cde40ee84a581dbdcdabe77ef", + "comment": "Introduced new tag @packagedocumentation which replaces the earlier approach that used a \"packageDescription\" variable" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.24` to `0.3.25`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.12` to `2.2.13`" + } + ] + } + }, + { + "version": "4.3.7", + "tag": "@microsoft/api-extractor_v4.3.7", + "date": "Fri, 05 Jan 2018 20:26:45 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.23` to `0.3.24`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.11` to `2.2.12`" + } + ] + } + }, + { + "version": "4.3.6", + "tag": "@microsoft/api-extractor_v4.3.6", + "date": "Fri, 05 Jan 2018 00:48:41 GMT", + "comments": { + "patch": [ + { + "author": "Nicholas Pape ", + "commit": "b942f54445bf01dbdb98aaa5f0007f5b95315b58", + "comment": "Update Jest to ~21.2.1" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.22` to `0.3.23`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.10` to `2.2.11`" + } + ] + } + }, + { + "version": "4.3.5", + "tag": "@microsoft/api-extractor_v4.3.5", + "date": "Fri, 22 Dec 2017 17:04:46 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "e27707503756cbfb2e9332833b987009c32b8198", + "comment": "Fixed an issue where warnings would cause the api-extractor tool to return a nonzero exit code for a \"--local\" build; warnings should not fail the build in this scenario" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.21` to `0.3.22`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.9` to `2.2.10`" + } + ] + } + }, + { + "version": "4.3.4", + "tag": "@microsoft/api-extractor_v4.3.4", + "date": "Tue, 12 Dec 2017 03:33:26 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.20` to `0.3.21`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.8` to `2.2.9`" + } + ] + } + }, + { + "version": "4.3.3", + "tag": "@microsoft/api-extractor_v4.3.3", + "date": "Thu, 30 Nov 2017 23:59:09 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.19` to `0.3.20`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.7` to `2.2.8`" + } + ] + } + }, + { + "version": "4.3.2", + "tag": "@microsoft/api-extractor_v4.3.2", + "date": "Thu, 30 Nov 2017 23:12:21 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.18` to `0.3.19`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.6` to `2.2.7`" + } + ] + } + }, + { + "version": "4.3.1", + "tag": "@microsoft/api-extractor_v4.3.1", + "date": "Wed, 29 Nov 2017 17:05:37 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.17` to `0.3.18`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.5` to `2.2.6`" + } + ] + } + }, + { + "version": "4.3.0", + "tag": "@microsoft/api-extractor_v4.3.0", + "date": "Tue, 28 Nov 2017 23:43:55 GMT", + "comments": { + "minor": [ + { + "author": "pgonzal ", + "commit": "aa6a9d0000c787f37623ae434cccbb5f0fdb7f29", + "comment": "Add Extractor.processProject() whose return value indicates success" + } + ], + "patch": [ + { + "author": "pgonzal ", + "commit": "aa6a9d0000c787f37623ae434cccbb5f0fdb7f29", + "comment": "Deprecate Extractor.analyzeProject() API" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.16` to `0.3.17`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.4` to `2.2.5`" + } + ] + } + }, + { + "version": "4.2.6", + "tag": "@microsoft/api-extractor_v4.2.6", + "date": "Mon, 13 Nov 2017 17:04:50 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.15` to `0.3.16`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.3` to `2.2.4`" + } + ] + } + }, + { + "version": "4.2.5", + "tag": "@microsoft/api-extractor_v4.2.5", + "date": "Mon, 06 Nov 2017 17:04:18 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.14` to `0.3.15`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.2` to `2.2.3`" + } + ] + } + }, + { + "version": "4.2.4", + "tag": "@microsoft/api-extractor_v4.2.4", + "date": "Thu, 02 Nov 2017 16:05:24 GMT", + "comments": { + "patch": [ + { + "author": "QZ ", + "commit": "2c58095f2f13492887cc1278c9a0cff49af9735b", + "comment": "lock the reference version between web build tools projects" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `0.3.13` to `0.3.14`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `2.2.1` to `2.2.2`" + } + ] + } + }, + { + "version": "4.2.3", + "tag": "@microsoft/api-extractor_v4.2.3", + "date": "Wed, 01 Nov 2017 21:06:08 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "e449bd6cdc3c179461be68e59590c25021cd1286", + "comment": "Upgrade cyclic dependencies" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.12` to `~0.3.13`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.2.0` to `~2.2.1`" + } + ] + } + }, + { + "version": "4.2.2", + "tag": "@microsoft/api-extractor_v4.2.2", + "date": "Tue, 31 Oct 2017 21:04:04 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.11` to `~0.3.12`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.1.4` to `~2.2.0`" + } + ] + } + }, + { + "version": "4.2.1", + "tag": "@microsoft/api-extractor_v4.2.1", + "date": "Tue, 31 Oct 2017 16:04:55 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.10` to `~0.3.11`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.1.3` to `~2.1.4`" + } + ] + } + }, + { + "version": "4.2.0", + "tag": "@microsoft/api-extractor_v4.2.0", + "date": "Wed, 25 Oct 2017 20:03:59 GMT", + "comments": { + "minor": [ + { + "author": "pgonzal ", + "commit": "6cc203e0282130eb7c9a40906c1a8861c763d56c", + "comment": "Improved the way API JSON represents documentation markup; this is a file format change" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.9` to `~0.3.10`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.1.2` to `~2.1.3`" + } + ] + } + }, + { + "version": "4.1.2", + "tag": "@microsoft/api-extractor_v4.1.2", + "date": "Tue, 24 Oct 2017 18:17:12 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.8` to `~0.3.9`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.1.1` to `~2.1.2`" + } + ] + } + }, + { + "version": "4.1.1", + "tag": "@microsoft/api-extractor_v4.1.1", + "date": "Mon, 23 Oct 2017 21:53:12 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "5de032b254b632b8af0d0dd98913acef589f88d5", + "comment": "Updated cyclic dependencies" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.7` to `~0.3.8`" + }, + { + "comment": "Updating dependency \"@microsoft/ts-command-line\" from `~2.1.0` to `~2.1.1`" + } + ] + } + }, + { + "version": "4.1.0", + "tag": "@microsoft/api-extractor_v4.1.0", + "date": "Fri, 20 Oct 2017 19:57:12 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "3c4f7aeb43999a968da41a8a2a2acbc3410f4ae9", + "comment": "Fixed an issue where properties were sometimes marked as readonly; a remark is automatically generated for classes with internal constructors" + } + ], + "minor": [ + { + "author": "pgonzal ", + "commit": "e37c63a2810a22957806bfe6a607b6c925244b7a", + "comment": "Add policies.namespaceSupport option to API Extractor config" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.6` to `~0.3.7`" + } + ] + } + }, + { + "version": "4.0.1", + "tag": "@microsoft/api-extractor_v4.0.1", + "date": "Fri, 20 Oct 2017 01:52:54 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "7f9f66ba071e2bcf77f224f784c14efbe627ba40", + "comment": "Rename ApiExtractor class to Extractor" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.5` to `~0.3.6`" + } + ] + } + }, + { + "version": "4.0.0", + "tag": "@microsoft/api-extractor_v4.0.0", + "date": "Fri, 20 Oct 2017 01:04:44 GMT", + "comments": { + "major": [ + { + "author": "pgonzal ", + "commit": "3ff332dc81aafca27952120afc1be0788239b741", + "comment": "Redesigned interface for invoking API Extractor" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.4` to `~0.3.5`" + } + ] + } + }, + { + "version": "3.4.2", + "tag": "@microsoft/api-extractor_v3.4.2", + "date": "Thu, 05 Oct 2017 01:05:02 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.2` to `~0.3.3`" + } + ] + } + }, + { + "version": "3.4.1", + "tag": "@microsoft/api-extractor_v3.4.1", + "date": "Fri, 29 Sep 2017 01:03:42 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "5d78825558e12fa828d114a655975c315aa1a566", + "comment": "Removed IMarkupPage.docId" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.1` to `~0.3.2`" + } + ] + } + }, + { + "version": "3.4.0", + "tag": "@microsoft/api-extractor_v3.4.0", + "date": "Thu, 28 Sep 2017 01:04:28 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "8e0010db430e7d6b84d12f38c5c1669c94ccf30e", + "comment": "The *.api.json \"linkDocElement\" type now always explicitly specifies the package name, rather than expecting the reader to infer it" + }, + { + "author": "pgonzal ", + "commit": "8e0010db430e7d6b84d12f38c5c1669c94ccf30e", + "comment": "The *.api.json file format now exposes \"signature\" information for properties, functions, and module variables" + } + ], + "minor": [ + { + "author": "pgonzal ", + "commit": "0309dea3eb9f78162f85d59ae4fbf5f1b6f4ea31", + "comment": "Skipping two lines in an AEDoc comment now creates a paragraph separator for the generated documentation" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.3.0` to `~0.3.1`" + } + ] + } + }, + { + "version": "3.3.0", + "tag": "@microsoft/api-extractor_v3.3.0", + "date": "Fri, 22 Sep 2017 01:04:02 GMT", + "comments": { + "minor": [ + { + "author": "Nick Pape ", + "commit": "481a10f460a454fb5a3e336e3cf25a1c3f710645", + "comment": "Upgrade to es6" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.11` to `~0.3.0`" + } + ] + } + }, + { + "version": "3.2.6", + "tag": "@microsoft/api-extractor_v3.2.6", + "date": "Wed, 20 Sep 2017 22:10:17 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.10` to `~0.2.11`" + } + ] + } + }, + { + "version": "3.2.5", + "tag": "@microsoft/api-extractor_v3.2.5", + "date": "Mon, 11 Sep 2017 13:04:55 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "d4271683f40917843650ddb537dcd21f1b3372be", + "comment": "The isBeta and deprecatedMessage fields are now inherited in the *.api.json files" + }, + { + "author": "pgonzal ", + "commit": "7de7daf14ea8299922e578125669ecfabbde8857", + "comment": "Fix an issue where the *.api.json file was sometimes missing function parameters" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.9` to `~0.2.10`" + } + ] + } + }, + { + "version": "3.2.4", + "tag": "@microsoft/api-extractor_v3.2.4", + "date": "Fri, 08 Sep 2017 01:28:04 GMT", + "comments": { + "patch": [ + { + "author": "Nick Pape ", + "commit": "bb96549aa8508ff627a0cae5ee41ae0251f2777d", + "comment": "Deprecate @types/es6-coll ections in favor of built-in typescript typings 'es2015.collection' a nd 'es2015.iterable'" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.7` to `~0.2.8`" + } + ] + } + }, + { + "version": "3.2.3", + "tag": "@microsoft/api-extractor_v3.2.3", + "date": "Thu, 07 Sep 2017 13:04:35 GMT", + "comments": { + "patch": [ + { + "author": "pgonzal ", + "commit": "a8b0022b8912d6fb07d1e0dd6618f8842ad6e86c", + "comment": "Fix incorrect schema/typings for enum members" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.6` to `~0.2.7`" + } + ] + } + }, + { + "version": "3.2.2", + "tag": "@microsoft/api-extractor_v3.2.2", + "date": "Thu, 07 Sep 2017 00:11:11 GMT", + "comments": { + "patch": [ + { + "author": "Nick Pape ", + "commit": "4b7451b442c2078a96430f7a05caed37101aed52", + "comment": " Add $schema field to all schemas" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.5` to `~0.2.6`" + } + ] + } + }, + { + "version": "3.2.1", + "tag": "@microsoft/api-extractor_v3.2.1", + "date": "Wed, 06 Sep 2017 13:03:42 GMT", + "comments": { + "patch": [ + { + "comment": "Converted IMarkupDocumentationLink to IMarkupApiLink, which exposes the underlying IApiItemReference rather than assuming a particular \"document ID\" model" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.4` to `~0.2.5`" + } + ] + } + }, + { + "version": "3.2.0", + "tag": "@microsoft/api-extractor_v3.2.0", + "date": "Tue, 05 Sep 2017 19:03:56 GMT", + "comments": { + "minor": [ + { + "comment": "Add the constructor signature and package name to the exported API signature" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.3` to `~0.2.4`" + } + ] + } + }, + { + "version": "3.1.0", + "tag": "@microsoft/api-extractor_v3.1.0", + "date": "Sat, 02 Sep 2017 01:04:26 GMT", + "comments": { + "minor": [ + { + "comment": "Expanded the api-extractor API to expose interfaces for the *.api.json file fileformat" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.2` to `~0.2.3`" + } + ] + } + }, + { + "version": "3.0.0", + "tag": "@microsoft/api-extractor_v3.0.0", + "date": "Thu, 31 Aug 2017 18:41:18 GMT", + "comments": { + "major": [ + { + "comment": "Fix compatibility issues with old releases, by incrementing the major version number" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.1` to `~0.2.2`" + } + ] + } + }, + { + "version": "2.3.7", + "tag": "@microsoft/api-extractor_v2.3.7", + "date": "Thu, 31 Aug 2017 17:46:25 GMT", + "comments": { + "patch": [ + { + "comment": "Fix issue where node-core-library was not an explicit dependency" + } + ], + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.2.0` to `~0.2.1`" + } + ] + } + }, + { + "version": "2.3.6", + "tag": "@microsoft/api-extractor_v2.3.6", + "date": "Wed, 30 Aug 2017 01:04:34 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-core-library\" from `~0.1.3` to `~0.2.0`" + } + ] + } + }, + { + "version": "2.3.5", + "tag": "@microsoft/api-extractor_v2.3.5", + "date": "Thu, 24 Aug 2017 22:44:12 GMT", + "comments": { + "patch": [ + { + "comment": "Update the schema validator." + } + ] + } + }, + { + "version": "2.3.4", + "tag": "@microsoft/api-extractor_v2.3.4", + "date": "Thu, 24 Aug 2017 01:04:33 GMT", + "comments": {} + }, + { + "version": "2.3.3", + "tag": "@microsoft/api-extractor_v2.3.3", + "date": "Tue, 22 Aug 2017 13:04:22 GMT", + "comments": { + "patch": [ + { + "comment": "Added \"api-documenter\" code sample" + } + ] + } + }, + { + "version": "2.3.2", + "tag": "@microsoft/api-extractor_v2.3.2", + "date": "Tue, 15 Aug 2017 01:29:31 GMT", + "comments": { + "patch": [ + { + "comment": "Introduce Span parser for upcoming *.d.ts generator" + } + ] + } + }, + { + "version": "2.3.1", + "tag": "@microsoft/api-extractor_v2.3.1", + "date": "Thu, 27 Jul 2017 01:04:48 GMT", + "comments": { + "patch": [ + { + "comment": "Upgrade to the TS2.4 version of the build tools." + } + ] + } + }, + { + "version": "2.3.0", + "tag": "@microsoft/api-extractor_v2.3.0", + "date": "Tue, 25 Jul 2017 20:03:31 GMT", + "comments": { + "minor": [ + { + "comment": "Upgrade to TypeScript 2.4" + } + ] + } + }, + { + "version": "2.2.0", + "tag": "@microsoft/api-extractor_v2.2.0", + "date": "Wed, 21 Jun 2017 04:19:35 GMT", + "comments": { + "minor": [ + { + "comment": "Add two new features: An error is reported if a top-level definition is missing its release tag. The constructor summary will now be autogenerated if omitted." + } + ] + } + }, + { + "version": "2.0.10", + "tag": "@microsoft/api-extractor_v2.0.10", + "date": "Tue, 20 Jun 2017 01:04:54 GMT", + "comments": { + "patch": [ + { + "comment": "Improve the wording of many error messages" + }, + { + "comment": "Fix a bug with parsing of @link tags" + }, + { + "comment": "Issue warnings for @internal definitions that are not prefixed with an underscore" + } + ] + } + }, + { + "version": "2.0.9", + "tag": "@microsoft/api-extractor_v2.0.9", + "date": "Sat, 17 Jun 2017 01:02:59 GMT", + "comments": { + "patch": [ + { + "comment": "The unsupported @summary tag is now reported as an error" + }, + { + "comment": "Use a cache to speed up package.json lookups" + } + ] + } + }, + { + "version": "2.0.8", + "tag": "@microsoft/api-extractor_v2.0.8", + "date": "Wed, 14 Jun 2017 13:03:40 GMT", + "comments": { + "patch": [ + { + "comment": "Definitions marked as @beta are now included in the *.api.json files for documentation" + } + ] + } + }, + { + "version": "2.0.7", + "tag": "@microsoft/api-extractor_v2.0.7", + "date": "Thu, 08 Jun 2017 05:15:52 GMT", + "comments": { + "patch": [ + { + "comment": "Updated README.md" + } + ] + } + }, + { + "version": "2.0.6", + "tag": "@microsoft/api-extractor_v2.0.6", + "date": "Mon, 15 May 2017 21:59:43 GMT", + "comments": { + "patch": [ + { + "comment": "Added support for Namespace with ApiNamespace" + } + ] + } + }, + { + "version": "2.0.5", + "tag": "@microsoft/api-extractor_v2.0.5", + "date": "Sat, 22 Apr 2017 01:02:03 GMT", + "comments": { + "patch": [ + { + "comment": "Added check for API names that are not supported (only letters and numbers supported)" + } + ] + } + }, + { + "version": "2.0.4", + "tag": "@microsoft/api-extractor_v2.0.4", + "date": "Wed, 19 Apr 2017 20:18:06 GMT", + "comments": { + "patch": [ + { + "comment": "Remove ES6 Promise & @types/es6-promise typings" + } + ] + } + }, + { + "version": "2.0.3", + "tag": "@microsoft/api-extractor_v2.0.3", + "date": "Fri, 14 Apr 2017 17:44:08 GMT", + "comments": { + "patch": [ + { + "comment": "Added collect references ability to detect determine type information of return types and parameter types." + } + ] + } + }, + { + "version": "2.0.2", + "tag": "@microsoft/api-extractor_v2.0.2", + "date": "Fri, 07 Apr 2017 21:43:16 GMT", + "comments": { + "patch": [ + { + "comment": "Adjusted the version specifier for typescript to ~2.2.2" + } + ] + } + }, + { + "version": "2.0.1", + "tag": "@microsoft/api-extractor_v2.0.1", + "date": "Thu, 06 Apr 2017 01:32:23 GMT", + "comments": { + "patch": [ + { + "comment": "Removed hard coding of @public for ApiPackage" + } + ] + } + }, + { + "version": "2.0.0", + "tag": "@microsoft/api-extractor_v2.0.0", + "date": "Mon, 20 Mar 2017 21:52:20 GMT", + "comments": { + "major": [ + { + "comment": "Fixing whitespace, also a variable that was shadowing another variable." + } + ] + } + }, + { + "version": "1.1.19", + "tag": "@microsoft/api-extractor_v1.1.19", + "date": "Mon, 20 Mar 2017 04:20:13 GMT", + "comments": { + "patch": [ + { + "comment": "Reverting change." + } + ] + } + }, + { + "version": "1.1.18", + "tag": "@microsoft/api-extractor_v1.1.18", + "date": "Mon, 20 Mar 2017 03:50:55 GMT", + "comments": { + "patch": [ + { + "comment": "Reverting previous change, which causes a regression in SPFx yeoman sc enario." + } + ] + } + }, + { + "version": "1.1.17", + "tag": "@microsoft/api-extractor_v1.1.17", + "date": "Mon, 20 Mar 2017 00:54:03 GMT", + "comments": { + "patch": [ + { + "comment": "Fixing lint whitespace issues." + } + ] + } + }, + { + "version": "1.1.16", + "tag": "@microsoft/api-extractor_v1.1.16", + "date": "Sun, 19 Mar 2017 19:10:30 GMT", + "comments": { + "patch": [ + { + "comment": "Fixing variable that was shadowing another variable." + } + ] + } + }, + { + "version": "1.1.15", + "tag": "@microsoft/api-extractor_v1.1.15", + "date": "Wed, 15 Mar 2017 01:32:09 GMT", + "comments": { + "patch": [ + { + "comment": "Locking `@types` packages. Synchronizing version specifiers for dependencies with other `web-build-tools` projects." + } + ] + } + }, + { + "version": "1.1.14", + "tag": "@microsoft/api-extractor_v1.1.14", + "date": "Sat, 18 Feb 2017 02:32:06 GMT", + "comments": { + "patch": [ + { + "comment": "Seperated the ApiItem initialization into 3 stages: create documentation that doesn't require resolution, then complete initialization by resolving links and inheritdocs. This allows us to ignore harmless cycles like type references\"" + } + ] + } + }, + { + "version": "1.1.13", + "tag": "@microsoft/api-extractor_v1.1.13", + "date": "Thu, 16 Feb 2017 22:10:39 GMT", + "comments": { + "patch": [ + { + "comment": "Fixed Api-Extractor error message, changed apostrophe to backtick." + } + ] + } + }, + { + "version": "1.1.12", + "tag": "@microsoft/api-extractor_v1.1.12", + "date": "Thu, 16 Feb 2017 18:56:57 GMT", + "comments": { + "patch": [ + { + "comment": "Added support for local API definition resolution\"" + } + ] + } + }, + { + "version": "1.1.11", + "tag": "@microsoft/api-extractor_v1.1.11", + "date": "Sat, 11 Feb 2017 02:32:35 GMT", + "comments": { + "patch": [ + { + "comment": "Changed dependency for ApiDocumentation to abstract the resolving of API definition references." + } + ] + } + }, + { + "version": "1.1.10", + "tag": "@microsoft/api-extractor_v1.1.10", + "date": "Fri, 10 Feb 2017 20:01:30 GMT", + "comments": { + "patch": [ + { + "comment": " Added support to not throw error, instead report error if no type is declared on properties and parameters" + } + ] + } + }, + { + "version": "1.1.9", + "tag": "@microsoft/api-extractor_v1.1.9", + "date": "Tue, 07 Feb 2017 20:37:06 GMT", + "comments": { + "patch": [ + { + "comment": "Fixing issue where undocumented comment was not being emitted." + } + ] + } + }, + { + "version": "1.1.8", + "tag": "@microsoft/api-extractor_v1.1.8", + "date": "Sat, 04 Feb 2017 02:32:05 GMT", + "comments": { + "patch": [ + { + "comment": "Moved ApiItem references within ApiDocumentation, to ApiItem caller." + } + ] + } + }, + { + "version": "1.1.7", + "tag": "@microsoft/api-extractor_v1.1.7", + "date": "Thu, 02 Feb 2017 14:05:53 GMT", + "comments": { + "patch": [ + { + "comment": "Refactored ApiDocumentation creation to resolve references method." + } + ] + } + }, + { + "version": "1.1.6", + "tag": "@microsoft/api-extractor_v1.1.6", + "date": "Wed, 01 Feb 2017 20:09:30 GMT", + "comments": { + "patch": [ + { + "comment": "Added ApiItemKind enum and refactored child classes." + } + ] + } + }, + { + "version": "1.1.5", + "tag": "@microsoft/api-extractor_v1.1.5", + "date": "Fri, 27 Jan 2017 20:04:15 GMT", + "comments": { + "patch": [ + { + "comment": "Changed name of Analyzer to Extractor, added support for external api json doc loading." + } + ] + } + }, + { + "version": "1.1.4", + "tag": "@microsoft/api-extractor_v1.1.4", + "date": "Fri, 27 Jan 2017 02:35:10 GMT", + "comments": { + "patch": [ + { + "comment": "Added ExternalApiHelper class to be used in generating api documentation json files for external types." + }, + { + "comment": "Added description for packages implementation." + }, + { + "comment": "Added config folder with file to enable api-extractor on itself. rebuild project on previous build." + } + ] + } + }, + { + "version": "1.1.3", + "tag": "@microsoft/api-extractor_v1.1.3", + "date": "Tue, 24 Jan 2017 01:36:35 GMT", + "comments": { + "patch": [ + { + "comment": "Json schema was updated to reflect feature additions to linkDocElement. The linkDocElement can now be of type 'code' which refers to an API definition reference." + } + ] + } + }, + { + "version": "1.1.2", + "tag": "@microsoft/api-extractor_v1.1.2", + "date": "Fri, 20 Jan 2017 01:46:41 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-library-build\" from `~2.1.0` to `~2.2.0`" + } + ] + } + }, + { + "version": "1.1.1", + "tag": "@microsoft/api-extractor_v1.1.1", + "date": "Thu, 19 Jan 2017 20:04:40 GMT", + "comments": { + "patch": [ + { + "comment": "Check for missing JSDoc sequences changed." + }, + { + "comment": "Improved error messages" + } + ] + } + }, + { + "version": "1.1.0", + "tag": "@microsoft/api-extractor_v1.1.0", + "date": "Wed, 18 Jan 2017 20:04:29 GMT", + "comments": { + "minor": [ + { + "comment": "Updating API Extractor to work with TypeScript 2.1" + } + ] + } + }, + { + "version": "1.0.2", + "tag": "@microsoft/api-extractor_v1.0.2", + "date": "Mon, 16 Jan 2017 20:04:15 GMT", + "comments": { + "patch": [ + { + "comment": "@link capability for href and API definition references" + } + ] + } + }, + { + "version": "1.0.1", + "tag": "@microsoft/api-extractor_v1.0.1", + "date": "Fri, 13 Jan 2017 06:46:05 GMT", + "comments": { + "dependency": [ + { + "comment": "Updating dependency \"@microsoft/node-library-build\" from `~2.0.0` to `~2.1.0`" + } + ] + } + }, + { + "version": "1.0.0", + "tag": "@microsoft/api-extractor_v1.0.0", + "date": "Wed, 11 Jan 2017 14:11:26 GMT", + "comments": { + "major": [ + { + "comment": "Introducing API Extractor" + } + ] + } + } + ] +} diff --git a/packages/api-extractor/CHANGELOG.md b/packages/api-extractor/CHANGELOG.md new file mode 100644 index 000000000..b1987f606 --- /dev/null +++ b/packages/api-extractor/CHANGELOG.md @@ -0,0 +1,2784 @@ +# Change Log - @microsoft/api-extractor + +This log was last generated on Mon, 30 Oct 2023 23:36:38 GMT and should not be manually modified. + +## 7.38.1 +Mon, 30 Oct 2023 23:36:38 GMT + +_Version update only_ + +## 7.38.0 +Sun, 01 Oct 2023 02:56:29 GMT + +### Minor changes + +- Add a new message "ae-undocumented" to support logging of undocumented API items + +## 7.37.3 +Sat, 30 Sep 2023 00:20:51 GMT + +### Patches + +- Don't strip out @alpha items when generating API reports. + +## 7.37.2 +Thu, 28 Sep 2023 20:53:16 GMT + +_Version update only_ + +## 7.37.1 +Tue, 26 Sep 2023 09:30:33 GMT + +### Patches + +- Update type-only imports to include the type modifier. + +## 7.37.0 +Fri, 15 Sep 2023 00:36:58 GMT + +### Minor changes + +- Update @types/node from 14 to 18 + +## 7.36.4 +Tue, 08 Aug 2023 07:10:39 GMT + +_Version update only_ + +## 7.36.3 +Wed, 19 Jul 2023 00:20:31 GMT + +### Patches + +- Updated semver dependency + +## 7.36.2 +Wed, 12 Jul 2023 15:20:39 GMT + +### Patches + +- Add api-extractor support for .d.mts and .d.cts files + +## 7.36.1 +Thu, 06 Jul 2023 00:16:19 GMT + +_Version update only_ + +## 7.36.0 +Mon, 19 Jun 2023 22:40:21 GMT + +### Minor changes + +- Use the `IRigConfig` interface in the `IExtractorConfigLoadForFolderOptions` object insteacd of the `RigConfig` class. + +## 7.35.4 +Thu, 15 Jun 2023 00:21:01 GMT + +_Version update only_ + +## 7.35.3 +Tue, 13 Jun 2023 01:49:01 GMT + +_Version update only_ + +## 7.35.2 +Wed, 07 Jun 2023 22:45:16 GMT + +_Version update only_ + +## 7.35.1 +Mon, 29 May 2023 15:21:15 GMT + +_Version update only_ + +## 7.35.0 +Mon, 22 May 2023 06:34:32 GMT + +### Minor changes + +- Upgrade the TypeScript dependency to ~5.0.4 + +## 7.34.9 +Fri, 12 May 2023 00:23:05 GMT + +_Version update only_ + +## 7.34.8 +Thu, 04 May 2023 00:20:28 GMT + +_Version update only_ + +## 7.34.7 +Mon, 01 May 2023 15:23:20 GMT + +_Version update only_ + +## 7.34.6 +Sat, 29 Apr 2023 00:23:03 GMT + +_Version update only_ + +## 7.34.5 +Thu, 27 Apr 2023 17:18:42 GMT + +_Version update only_ + +## 7.34.4 +Fri, 10 Feb 2023 01:18:50 GMT + +_Version update only_ + +## 7.34.3 +Sun, 05 Feb 2023 03:02:02 GMT + +_Version update only_ + +## 7.34.2 +Wed, 01 Feb 2023 02:16:34 GMT + +_Version update only_ + +## 7.34.1 +Mon, 30 Jan 2023 16:22:30 GMT + +_Version update only_ + +## 7.34.0 +Wed, 25 Jan 2023 07:26:55 GMT + +### Minor changes + +- Add new .api.json field `isAbstract` to track `abstract` modifier in ApiClass, ApiMethod, and ApiProperty via ApiAbstractMixin (GitHub #3661) + +## 7.33.8 +Wed, 18 Jan 2023 22:44:12 GMT + +### Patches + +- Use ts.getCheckFlags to fix TS 5.0 + +## 7.33.7 +Fri, 09 Dec 2022 16:18:28 GMT + +_Version update only_ + +## 7.33.6 +Tue, 08 Nov 2022 01:20:55 GMT + +_Version update only_ + +## 7.33.5 +Wed, 26 Oct 2022 00:16:16 GMT + +### Patches + +- Update the @microsoft/tsdoc dependency version to 0.14.2. + +## 7.33.4 +Mon, 17 Oct 2022 22:14:21 GMT + +_Version update only_ + +## 7.33.3 +Mon, 17 Oct 2022 15:16:00 GMT + +### Patches + +- Fix a regression where the "fileUrlPath" property would contain a malformed path when API Extractor is run on Windows. + +## 7.33.2 +Fri, 14 Oct 2022 15:26:31 GMT + +### Patches + +- Fix references from computed properties #3629 + +## 7.33.1 +Thu, 13 Oct 2022 00:20:15 GMT + +_Version update only_ + +## 7.33.0 +Tue, 11 Oct 2022 23:49:12 GMT + +### Minor changes + +- Extract the original source file path for relevant API items and add a new projectFolderUrl setting to the api-extractor.json config that allows one to specify what URL their project folder can be found at. + +## 7.32.1 +Mon, 10 Oct 2022 15:23:44 GMT + +_Version update only_ + +## 7.32.0 +Thu, 29 Sep 2022 07:13:06 GMT + +### Minor changes + +- Update parser to TypeScript 4.8. + +## 7.31.2 +Wed, 21 Sep 2022 20:21:10 GMT + +_Version update only_ + +## 7.31.1 +Thu, 15 Sep 2022 00:18:51 GMT + +_Version update only_ + +## 7.31.0 +Tue, 13 Sep 2022 00:16:55 GMT + +### Minor changes + +- Fix an issue where aliased classes sometimes had incorrect canonical references in *.api.json (GitHub #3593) + +## 7.30.1 +Mon, 12 Sep 2022 22:27:48 GMT + +### Patches + +- Fix a recent regression where items exported from both the entry point and from an exported namespace appeared only once in the API doc model (GitHub #3619) + +## 7.30.0 +Fri, 02 Sep 2022 17:48:42 GMT + +### Minor changes + +- Add new "apiReport.includeForgottenExports" and "docModel.includeForgottenExports" properties to control whether forgotten exports are included in the API report and doc model files. +- Fix incorrect declaration references for symbols not exported from the package's entry point. + +## 7.29.5 +Wed, 24 Aug 2022 03:01:22 GMT + +_Version update only_ + +## 7.29.4 +Wed, 24 Aug 2022 00:14:38 GMT + +### Patches + +- Remove use of LegacyAdapters.sortStable + +## 7.29.3 +Fri, 19 Aug 2022 00:17:19 GMT + +_Version update only_ + +## 7.29.2 +Wed, 10 Aug 2022 09:52:12 GMT + +### Patches + +- Fix incorrect declaration references for local symbols within namespaces + +## 7.29.1 +Wed, 10 Aug 2022 08:12:16 GMT + +### Patches + +- Fix a regression where .api.json excerpts were sometimes missing tokens (GitHub #3561), and generally improve the quality of excerpt generation + +## 7.29.0 +Wed, 03 Aug 2022 18:40:35 GMT + +### Minor changes + +- Upgrade TypeScript dependency to 4.7 + +## 7.28.7 +Mon, 01 Aug 2022 02:45:32 GMT + +_Version update only_ + +## 7.28.6 +Thu, 21 Jul 2022 23:30:27 GMT + +_Version update only_ + +## 7.28.5 +Thu, 21 Jul 2022 00:16:14 GMT + +_Version update only_ + +## 7.28.4 +Fri, 08 Jul 2022 15:17:46 GMT + +### Patches + +- Update api-extractor-template.json to "testMode" and "enumMemberOrder" comment sections. + +## 7.28.3 +Mon, 04 Jul 2022 15:15:13 GMT + +### Patches + +- Make enumMemberOrder configuration field optional + +## 7.28.2 +Thu, 30 Jun 2022 04:48:53 GMT + +### Patches + +- Improve logic that determines whether an API item is readonly + +## 7.28.1 +Tue, 28 Jun 2022 22:47:13 GMT + +_Version update only_ + +## 7.28.0 +Tue, 28 Jun 2022 00:23:32 GMT + +### Minor changes + +- Add support for the "ignoreMissingEntryPoint" ExtractorConfig option to allow for loading an ExtractorConfig before the target project is built. + +## 7.27.1 +Mon, 27 Jun 2022 18:43:09 GMT + +_Version update only_ + +## 7.27.0 +Sat, 25 Jun 2022 21:00:40 GMT + +### Minor changes + +- API Extractor now populates an initializerTokenRange field for ApiProperty and ApiVariable items. + +## 7.26.1 +Sat, 25 Jun 2022 01:54:29 GMT + +_Version update only_ + +## 7.26.0 +Fri, 24 Jun 2022 07:16:47 GMT + +### Minor changes + +- Include new configuration option for preserving enum member order + +## 7.25.3 +Thu, 23 Jun 2022 22:14:24 GMT + +_Version update only_ + +## 7.25.2 +Fri, 17 Jun 2022 09:17:54 GMT + +_Version update only_ + +## 7.25.1 +Fri, 17 Jun 2022 00:16:18 GMT + +_Version update only_ + +## 7.25.0 +Tue, 07 Jun 2022 09:37:04 GMT + +### Minor changes + +- Add an "isReadonly" field to the doc model to indicate whether a property or variable is readonly +- Add an "isProtected" field to the doc model to indicate protected class members + +## 7.24.2 +Wed, 25 May 2022 22:25:07 GMT + +### Patches + +- Fix an issue where API Extractor would fail to run on a project where `"moduleResolution"` is set to `"Node16"` in `tsconfig.json` + +## 7.24.1 +Thu, 19 May 2022 15:13:20 GMT + +### Patches + +- Fix a recent regression that produced an error "Cannot read properties of undefined" (GitHub #3423) + +## 7.24.0 +Sat, 14 May 2022 03:01:27 GMT + +### Minor changes + +- Throw an error early if API Extractor will attempt to process non-.d.ts files +- Generate API doc model nodes for setters without getters + +### Patches + +- Address edge case in excerptBuilder token range logic + +## 7.23.2 +Tue, 10 May 2022 01:20:43 GMT + +_Version update only_ + +## 7.23.1 +Wed, 04 May 2022 23:29:13 GMT + +### Patches + +- Update the global variable analyzer to add support for changes to the TypeScript internals coming in v4.7 + +## 7.23.0 +Sat, 23 Apr 2022 02:13:06 GMT + +### Minor changes + +- Update to TypeScript 4.6 + +## 7.22.2 +Fri, 15 Apr 2022 00:12:36 GMT + +_Version update only_ + +## 7.22.1 +Wed, 13 Apr 2022 15:12:40 GMT + +_Version update only_ + +## 7.22.0 +Tue, 12 Apr 2022 23:29:34 GMT + +### Minor changes + +- Add an alphaTrimmedFilePath option that adds support for generating a DTS rollup that inclues @alpha, @beta, and @public members. + +## 7.21.3 +Tue, 12 Apr 2022 02:58:32 GMT + +### Patches + +- Update TSDoc dependencies. + +## 7.21.2 +Sat, 09 Apr 2022 19:07:47 GMT + +### Patches + +- Fix ambient modules bug caused by #3321. + +## 7.21.1 +Sat, 09 Apr 2022 02:24:26 GMT + +### Patches + +- Rename the "master" branch to "main". + +## 7.21.0 +Fri, 08 Apr 2022 20:05:59 GMT + +### Minor changes + +- Add support for projects that use tsconfig.json "baseUrl" and "paths" settings to remap imports of local files (GitHub #3291) + +## 7.20.1 +Wed, 06 Apr 2022 22:35:23 GMT + +### Patches + +- Fix an issue where .api.json excerpt text included extra whitespace (GitHub #3316) + +## 7.20.0 +Thu, 31 Mar 2022 02:06:05 GMT + +### Minor changes + +- Updated api-extractor to extract whether a parameter is optional. + +## 7.19.5 +Tue, 15 Mar 2022 19:15:53 GMT + +_Version update only_ + +## 7.19.4 +Wed, 05 Jan 2022 16:07:47 GMT + +_Version update only_ + +## 7.19.3 +Mon, 27 Dec 2021 16:10:40 GMT + +_Version update only_ + +## 7.19.2 +Thu, 09 Dec 2021 20:34:41 GMT + +_Version update only_ + +## 7.19.1 +Thu, 09 Dec 2021 00:21:54 GMT + +_Version update only_ + +## 7.19.0 +Wed, 08 Dec 2021 16:14:05 GMT + +### Minor changes + +- Update to TypeScript 4.5 + +## 7.18.21 +Mon, 06 Dec 2021 16:08:33 GMT + +_Version update only_ + +## 7.18.20 +Fri, 03 Dec 2021 03:05:22 GMT + +_Version update only_ + +## 7.18.19 +Sat, 06 Nov 2021 00:09:13 GMT + +_Version update only_ + +## 7.18.18 +Fri, 05 Nov 2021 15:09:18 GMT + +_Version update only_ + +## 7.18.17 +Wed, 27 Oct 2021 00:08:15 GMT + +### Patches + +- Update the package.json repository field to include the directory property. + +## 7.18.16 +Wed, 13 Oct 2021 15:09:54 GMT + +_Version update only_ + +## 7.18.15 +Fri, 08 Oct 2021 08:08:34 GMT + +_Version update only_ + +## 7.18.14 +Thu, 07 Oct 2021 07:13:35 GMT + +_Version update only_ + +## 7.18.13 +Tue, 05 Oct 2021 15:08:38 GMT + +_Version update only_ + +## 7.18.12 +Mon, 04 Oct 2021 15:10:18 GMT + +_Version update only_ + +## 7.18.11 +Fri, 24 Sep 2021 00:09:29 GMT + +_Version update only_ + +## 7.18.10 +Thu, 23 Sep 2021 00:10:40 GMT + +### Patches + +- Upgrade the `@types/node` dependency to version to version 12. + +## 7.18.9 +Tue, 14 Sep 2021 01:17:04 GMT + +_Version update only_ + +## 7.18.8 +Mon, 13 Sep 2021 15:07:05 GMT + +_Version update only_ + +## 7.18.7 +Fri, 27 Aug 2021 00:07:25 GMT + +_Version update only_ + +## 7.18.6 +Fri, 20 Aug 2021 15:08:10 GMT + +_Version update only_ + +## 7.18.5 +Wed, 11 Aug 2021 00:07:21 GMT + +_Version update only_ + +## 7.18.4 +Wed, 14 Jul 2021 15:06:29 GMT + +### Patches + +- Fix an issue where the .d.ts rollup sometimes used "default" as an identifier name causing a syntax error (GitHub #2804) + +## 7.18.3 +Tue, 13 Jul 2021 23:00:33 GMT + +### Patches + +- Revert a workaround for TypeScript issue #44422 which was fixed in 4.3.3 + +## 7.18.2 +Mon, 12 Jul 2021 23:08:26 GMT + +_Version update only_ + +## 7.18.1 +Thu, 08 Jul 2021 23:41:16 GMT + +### Patches + +- Fix a recent regression that reported "Internal Error: indentDocComment cannot be nested" (GitHub #2797) + +## 7.18.0 +Thu, 08 Jul 2021 06:00:48 GMT + +### Minor changes + +- Add support for import() type expressions (GitHub #1050) -- Thank you @javier-garcia-meteologica and @adventure-yunfei for solving this difficult problem! +- Improve formatting of declarations in .d.ts rollup and .api.md files, fixing some indentation issues + +## 7.17.1 +Thu, 01 Jul 2021 15:08:27 GMT + +_Version update only_ + +## 7.17.0 +Wed, 30 Jun 2021 15:06:54 GMT + +### Minor changes + +- Added support for "import * as module from './local/module';" (GitHub #1029) -- Big thanks to @adventure-yunfei, @mckn, @rbuckton, and @octogonz who all helped with this difficult PR! + +### Patches + +- Include /// directives in API report + +## 7.16.1 +Fri, 04 Jun 2021 19:59:53 GMT + +_Version update only_ + +## 7.16.0 +Fri, 04 Jun 2021 15:08:20 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 4.3 + +## 7.15.2 +Wed, 19 May 2021 00:11:39 GMT + +_Version update only_ + +## 7.15.1 +Mon, 03 May 2021 15:10:29 GMT + +_Version update only_ + +## 7.15.0 +Thu, 29 Apr 2021 23:26:50 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 4.2 + +## 7.14.0 +Tue, 20 Apr 2021 04:59:51 GMT + +### Minor changes + +- Projects can now define custom tags using a tsdoc.json file + +## 7.13.5 +Mon, 12 Apr 2021 15:10:28 GMT + +_Version update only_ + +## 7.13.4 +Thu, 08 Apr 2021 06:05:31 GMT + +_Version update only_ + +## 7.13.3 +Tue, 06 Apr 2021 15:14:22 GMT + +_Version update only_ + +## 7.13.2 +Thu, 04 Mar 2021 01:11:31 GMT + +_Version update only_ + +## 7.13.1 +Fri, 05 Feb 2021 16:10:42 GMT + +_Version update only_ + +## 7.13.0 +Wed, 13 Jan 2021 01:11:06 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 4.1 + +## 7.12.1 +Thu, 10 Dec 2020 23:25:49 GMT + +### Patches + +- Upgrade to TSDoc 0.12.24 + +## 7.12.0 +Wed, 18 Nov 2020 08:19:54 GMT + +### Minor changes + +- The "isOptional" .api.json field is now applied to both methods and properties + +## 7.11.5 +Wed, 18 Nov 2020 06:21:57 GMT + +### Patches + +- Update .api.json file format to store a new field "isOptional" for documenting optional properties + +## 7.11.4 +Wed, 11 Nov 2020 01:08:58 GMT + +_Version update only_ + +## 7.11.3 +Tue, 10 Nov 2020 23:13:12 GMT + +_Version update only_ + +## 7.11.2 +Fri, 30 Oct 2020 06:38:38 GMT + +_Version update only_ + +## 7.11.1 +Fri, 30 Oct 2020 00:10:14 GMT + +_Version update only_ + +## 7.11.0 +Thu, 29 Oct 2020 06:14:19 GMT + +### Minor changes + +- Upgrade the bundled compiler engine to TypeScript 4.0 + +## 7.10.6 +Wed, 28 Oct 2020 01:18:03 GMT + +_Version update only_ + +## 7.10.5 +Tue, 27 Oct 2020 15:10:13 GMT + +_Version update only_ + +## 7.10.4 +Tue, 06 Oct 2020 00:24:06 GMT + +_Version update only_ + +## 7.10.3 +Mon, 05 Oct 2020 22:36:57 GMT + +_Version update only_ + +## 7.10.2 +Mon, 05 Oct 2020 15:10:42 GMT + +_Version update only_ + +## 7.10.1 +Wed, 30 Sep 2020 18:39:17 GMT + +### Patches + +- Update to build with @rushstack/heft-node-rig + +## 7.10.0 +Wed, 30 Sep 2020 06:53:53 GMT + +### Minor changes + +- API Extractor now supports the config/rig.json system, as defined by @rushstack/rig-package +- Add IExtractorConfigPrepareOptions.projectFolderLookupToken +- Upgrade compiler; the API now requires TypeScript 3.9 or newer + +### Patches + +- Fix an InternalError reported when a declaration referred to itself using "tyepof" +- Update README.md + +## 7.9.22 +Tue, 22 Sep 2020 05:45:56 GMT + +_Version update only_ + +## 7.9.21 +Tue, 22 Sep 2020 01:45:31 GMT + +_Version update only_ + +## 7.9.20 +Tue, 22 Sep 2020 00:08:53 GMT + +_Version update only_ + +## 7.9.19 +Sat, 19 Sep 2020 04:37:26 GMT + +_Version update only_ + +## 7.9.18 +Sat, 19 Sep 2020 03:33:06 GMT + +_Version update only_ + +## 7.9.17 +Fri, 18 Sep 2020 22:57:24 GMT + +_Version update only_ + +## 7.9.16 +Fri, 18 Sep 2020 21:49:54 GMT + +_Version update only_ + +## 7.9.15 +Sun, 13 Sep 2020 01:53:20 GMT + +_Version update only_ + +## 7.9.14 +Fri, 11 Sep 2020 02:13:35 GMT + +_Version update only_ + +## 7.9.13 +Mon, 07 Sep 2020 07:37:37 GMT + +_Version update only_ + +## 7.9.12 +Sat, 05 Sep 2020 18:56:34 GMT + +_Version update only_ + +## 7.9.11 +Thu, 27 Aug 2020 11:27:06 GMT + +_Version update only_ + +## 7.9.10 +Mon, 24 Aug 2020 07:35:20 GMT + +_Version update only_ + +## 7.9.9 +Sat, 22 Aug 2020 05:55:42 GMT + +_Version update only_ + +## 7.9.8 +Fri, 21 Aug 2020 01:21:18 GMT + +_Version update only_ + +## 7.9.7 +Thu, 20 Aug 2020 15:13:53 GMT + +_Version update only_ + +## 7.9.6 +Tue, 18 Aug 2020 23:59:42 GMT + +_Version update only_ + +## 7.9.5 +Mon, 17 Aug 2020 04:53:23 GMT + +_Version update only_ + +## 7.9.4 +Wed, 12 Aug 2020 00:10:05 GMT + +### Patches + +- Updated project to build with Heft + +## 7.9.3 +Wed, 05 Aug 2020 18:27:32 GMT + +_Version update only_ + +## 7.9.2 +Thu, 09 Jul 2020 04:58:36 GMT + +### Patches + +- Fix an issue with handling of "export { default } from 'package';" (GitHub #2014) + +## 7.9.1 +Fri, 03 Jul 2020 15:09:04 GMT + +_Version update only_ + +## 7.9.0 +Fri, 03 Jul 2020 05:46:41 GMT + +### Minor changes + +- Add support for ECMAScript private fields (new in TypeScript 3.8) +- Add support for "import type" imports (new in TypeScript 3.8) +- Upgrade the bundled compiler engine to TypeScript 3.9 + +### Patches + +- Fix an issue where chained compiler errors were not formatted correctly +- Log the TypeScript bundled compiler version, and warn if it is outdated + +## 7.8.15 +Thu, 25 Jun 2020 06:43:35 GMT + +_Version update only_ + +## 7.8.14 +Wed, 24 Jun 2020 09:50:48 GMT + +_Version update only_ + +## 7.8.13 +Wed, 24 Jun 2020 09:04:28 GMT + +_Version update only_ + +## 7.8.12 +Mon, 15 Jun 2020 22:17:17 GMT + +### Patches + +- Fix an issue where documentation hyperlinks were sometimes missing when using the "bundledPackages" feature (GitHub #1933) + +## 7.8.11 +Wed, 10 Jun 2020 20:48:30 GMT + +_Version update only_ + +## 7.8.10 +Mon, 01 Jun 2020 08:34:17 GMT + +_Version update only_ + +## 7.8.9 +Sat, 30 May 2020 02:59:54 GMT + +_Version update only_ + +## 7.8.8 +Thu, 28 May 2020 05:59:02 GMT + +_Version update only_ + +## 7.8.7 +Wed, 27 May 2020 05:15:10 GMT + +_Version update only_ + +## 7.8.6 +Tue, 26 May 2020 23:00:25 GMT + +_Version update only_ + +## 7.8.5 +Fri, 22 May 2020 15:08:42 GMT + +_Version update only_ + +## 7.8.4 +Thu, 21 May 2020 23:09:44 GMT + +_Version update only_ + +## 7.8.3 +Thu, 21 May 2020 15:41:59 GMT + +_Version update only_ + +## 7.8.2 +Tue, 19 May 2020 15:08:19 GMT + +### Patches + +- Report an error to indicate that "import()" types are not supported + +## 7.8.1 +Fri, 15 May 2020 08:10:59 GMT + +_Version update only_ + +## 7.8.0 +Wed, 06 May 2020 08:23:45 GMT + +### Minor changes + +- Version update only + +## 7.7.13 +Wed, 08 Apr 2020 04:07:33 GMT + +_Version update only_ + +## 7.7.12 +Sun, 29 Mar 2020 00:04:12 GMT + +### Patches + +- Improve analysis of types exposed via global variables (fixes GitHub issues #1765, #1095, and #1316) + +## 7.7.11 +Sat, 28 Mar 2020 00:37:16 GMT + +### Patches + +- Upgrade to TSdoc 0.12.19 to fix an issue where `

` wasn't allowed as an HTML tag in a doc comment + +## 7.7.10 +Wed, 18 Mar 2020 15:07:47 GMT + +### Patches + +- Upgrade cyclic dependencies + +## 7.7.9 +Tue, 17 Mar 2020 23:55:58 GMT + +### Patches + +- Replace dependencies whose NPM scope was renamed from `@microsoft` to `@rushstack` + +## 7.7.8 +Tue, 28 Jan 2020 02:23:44 GMT + +_Version update only_ + +## 7.7.7 +Thu, 23 Jan 2020 01:07:56 GMT + +_Version update only_ + +## 7.7.6 +Tue, 21 Jan 2020 21:56:13 GMT + +_Version update only_ + +## 7.7.5 +Sun, 19 Jan 2020 02:26:52 GMT + +### Patches + +- Upgrade Node typings to Node 10 + +## 7.7.4 +Fri, 17 Jan 2020 01:08:23 GMT + +_Version update only_ + +## 7.7.3 +Tue, 14 Jan 2020 01:34:15 GMT + +### Patches + +- Fix an issue where "ae-incompatible-release-tags" was sometimes reported incorectly for property setters (GitHub #1681) + +## 7.7.2 +Thu, 09 Jan 2020 06:44:12 GMT + +### Patches + +- Fix an error "Cannot read property 'externalModuleIndicator' of undefined" (GitHub #1652) + +## 7.7.1 +Wed, 08 Jan 2020 00:11:31 GMT + +_Version update only_ + +## 7.7.0 +Tue, 03 Dec 2019 03:17:43 GMT + +### Minor changes + +- Improve declaration reference syntax to allow linking to overloaded functions/methods +- Fix an issue with TypeScript 3.7, which now emits separate signatures for property getters/setters + +## 7.6.2 +Sun, 24 Nov 2019 00:54:04 GMT + +_Version update only_ + +## 7.6.1 +Wed, 20 Nov 2019 06:14:28 GMT + +### Patches + +- Fix an issue where the newlineKind setting wasn't being applied correctly + +## 7.6.0 +Fri, 15 Nov 2019 04:50:50 GMT + +### Minor changes + +- Make newline type for generated files configurable + +## 7.5.6 +Mon, 11 Nov 2019 16:07:56 GMT + +_Version update only_ + +## 7.5.5 +Wed, 06 Nov 2019 22:44:18 GMT + +### Patches + +- Add support for TypeScript 3.7 + +## 7.5.4 +Tue, 05 Nov 2019 06:49:28 GMT + +### Patches + +- Fix an issue where API reports sometimes were ordered differently depending on the version of NodeJS (GitHub #1552) + +## 7.5.3 +Tue, 05 Nov 2019 01:08:39 GMT + +_Version update only_ + +## 7.5.2 +Tue, 22 Oct 2019 06:24:44 GMT + +_Version update only_ + +## 7.5.1 +Fri, 18 Oct 2019 15:15:01 GMT + +_Version update only_ + +## 7.5.0 +Sun, 06 Oct 2019 00:27:39 GMT + +### Minor changes + +- Allow separate release tags for overloaded functions and methods +- Add new api-extractor.json config setting "bundledPackages" + +## 7.4.7 +Fri, 04 Oct 2019 00:15:22 GMT + +### Patches + +- Fix an issue where IExtractorConfigPrepareOptions.packageJson was ignored (GitHub #1559) + +## 7.4.6 +Sun, 29 Sep 2019 23:56:29 GMT + +### Patches + +- Update repository URL + +## 7.4.5 +Wed, 25 Sep 2019 15:15:31 GMT + +_Version update only_ + +## 7.4.4 +Tue, 24 Sep 2019 02:58:49 GMT + +_Version update only_ + +## 7.4.3 +Mon, 23 Sep 2019 15:14:55 GMT + +_Version update only_ + +## 7.4.2 +Wed, 11 Sep 2019 19:56:23 GMT + +### Patches + +- Add support for an exported name that conflicts with a global name (GitHub #1350) + +## 7.4.1 +Tue, 10 Sep 2019 22:32:23 GMT + +### Patches + +- Update documentation + +## 7.4.0 +Tue, 10 Sep 2019 20:38:33 GMT + +### Minor changes + +- Add support for generating declaration references + +## 7.3.11 +Wed, 04 Sep 2019 18:28:06 GMT + +_Version update only_ + +## 7.3.10 +Wed, 04 Sep 2019 15:15:37 GMT + +### Patches + +- Update TSDoc dependency to 0.12.14 + +## 7.3.9 +Fri, 30 Aug 2019 00:14:32 GMT + +### Patches + +- Fix a problem where Unicode API names were not handled correctly + +## 7.3.8 +Mon, 12 Aug 2019 15:15:14 GMT + +_Version update only_ + +## 7.3.7 +Thu, 08 Aug 2019 15:14:17 GMT + +_Version update only_ + +## 7.3.6 +Thu, 08 Aug 2019 00:49:05 GMT + +### Patches + +- Fix an issue where a function with only one declaration was assigned an overloadIndex of 0 instead of 1 + +## 7.3.5 +Mon, 05 Aug 2019 22:04:32 GMT + +### Patches + +- Security updates. + +## 7.3.4 +Tue, 23 Jul 2019 01:13:01 GMT + +### Patches + +- ApiItem.name is now quoted when it contains invalid identifier characters, to avoid conflicts with an ECMAScript symbol expression + +## 7.3.3 +Mon, 22 Jul 2019 19:13:10 GMT + +### Patches + +- Update to use new api-extractor-model + +## 7.3.2 +Fri, 12 Jul 2019 19:12:46 GMT + +### Patches + +- Clarify docs for "--typescript-compiler-folder" + +## 7.3.1 +Thu, 11 Jul 2019 19:13:08 GMT + +### Patches + +- Add support for TypeScript 3.5 + +## 7.3.0 +Tue, 09 Jul 2019 19:13:24 GMT + +### Minor changes + +- Add a "--diagnostics" command-line option to help when troubleshooting problems + +## 7.2.3 +Mon, 08 Jul 2019 19:12:18 GMT + +### Patches + +- Fix a problem when analyzing .d.ts files that appear in the same folder as the corresponding .ts file (GitHub #1310) + +## 7.2.2 +Sat, 29 Jun 2019 02:30:10 GMT + +### Patches + +- Fix GitHub issue #1304 where "IExtractorInvokeOptions.typescriptCompilerFolder" did not work with TypeScript 3.4 + +## 7.2.1 +Wed, 12 Jun 2019 19:12:33 GMT + +_Version update only_ + +## 7.2.0 +Tue, 11 Jun 2019 00:48:06 GMT + +### Minor changes + +- Generate ApiTypeParameter entries and type alias types + +## 7.1.8 +Wed, 05 Jun 2019 19:12:34 GMT + +### Patches + +- Fix an issue where TSDoc index selectors (ApiParameterListMixin.overloadIndex) started from 0, whereas TSDoc requires a nonzero number + +## 7.1.7 +Tue, 04 Jun 2019 05:51:53 GMT + +### Patches + +- Upgrade api-extractor-model to remove ApiConstructor.isStatic, since TypeScript constructors cannot be static +- Improve handling of symbolic property and method names. + +## 7.1.6 +Mon, 27 May 2019 04:13:44 GMT + +### Patches + +- Fix incorrect path resolution for the "extends" field when loading tsconfig.json + +## 7.1.5 +Mon, 13 May 2019 02:08:35 GMT + +### Patches + +- Broaden support for default imports + +## 7.1.4 +Mon, 06 May 2019 20:46:21 GMT + +_Version update only_ + +## 7.1.3 +Mon, 06 May 2019 19:34:54 GMT + +### Patches + +- Add a new setting "omitTrimmingComments" to prevent extra comments from being emitted in the .d.ts rollup + +## 7.1.2 +Mon, 06 May 2019 19:11:16 GMT + +### Patches + +- Fix an issue where ExtractorResult.warningCount was not incremented for messages handled by IExtractorInvokeOptions.messageCallback (GitHub #1258) + +## 7.1.1 +Tue, 30 Apr 2019 23:08:02 GMT + +### Patches + +- Fix an issue where API signatures were sometimes truncated in the .api.json file (GitHub #1249) + +## 7.1.0 +Tue, 16 Apr 2019 11:01:37 GMT + +### Minor changes + +- Initial stable release of API Extractor 7 + +## 7.0.42 +Fri, 12 Apr 2019 06:13:16 GMT + +### Patches + +- Fix a regression that prevented certain types of warnings from being reported + +## 7.0.41 +Thu, 11 Apr 2019 07:14:01 GMT + +### Patches + +- THIS IS A RELEASE CANDIDATE FOR API-EXTRACTOR 7 +- (Breaking change) Rename "mainEntryPointFile" to "mainEntryPointFilePath" so all settings use a consistent naming convention +- (Breaking change) Paths that appear in api-extractor.json are now resolved relative to the config file unless prefixed with the `` token +- Add a new api-extractor.json setting "tsconfigFilePath" for customizing the tsconfig.json path +- Replace ExtractorConfig.packageJsonFullPath with ExtractorConfig.packageFolder +- Upgrade API Extractor to use TypeScript 3.4 for analysis + +## 7.0.40 +Tue, 09 Apr 2019 05:31:01 GMT + +### Patches + +- Improve the "--local" option to automatically create the API report file if it is missing + +## 7.0.39 +Mon, 08 Apr 2019 19:12:52 GMT + +### Patches + +- Rename "addToApiReviewFile" setting to "addToApiReportFile" + +## 7.0.38 +Sat, 06 Apr 2019 02:05:51 GMT + +### Patches + +- (Breaking change) Removed the ILogger API and renamed ExtractorMessageLogLevel to ExtractorLogLevel +- (Breaking change) Extractor console output is now modeled as ExtractorMessage objects and can be customized/filtered/handled by IExtractorInvokeOptions.messageCallback + +## 7.0.37 +Fri, 05 Apr 2019 04:16:16 GMT + +### Patches + +- Introduce "api-extractor init" command-line that helps enable API Extractor for a new project +- (Breaking change) Major redesign of the API used to invoke API Extractor +- (Breaking change) Major redesign of the api-extractor.json config file format +- Add a CompilerState API that allows an optimization where multiple invocations of Extractor can reuse the same TypeScript compiler analysis + +## 7.0.36 +Wed, 03 Apr 2019 02:58:33 GMT + +### Patches + +- Fix an issue where .d.ts.map file were sometimes mapped to the wrong location + +## 7.0.35 +Sat, 30 Mar 2019 22:27:16 GMT + +### Patches + +- Reintroduce the generated documentation notice for internal constructors +- Add limited support for resolving @inheritDoc references to external packages by postprocessing them in api-documenter + +## 7.0.34 +Thu, 28 Mar 2019 19:14:27 GMT + +### Patches + +- Validate `@link` tags and report a warning if the link cannot be resolved + +## 7.0.33 +Tue, 26 Mar 2019 20:54:18 GMT + +### Patches + +- Reintroduce support for `@inheritDoc` tags + +## 7.0.32 +Sat, 23 Mar 2019 03:48:31 GMT + +### Patches + +- If the TSDoc summary is missing for a class constructor, then automatically generate it +- Reintroduce support for the `@preapproved` TSDoc tag + +## 7.0.31 +Thu, 21 Mar 2019 04:59:11 GMT + +### Patches + +- Reintroduce "ae-internal-missing-underscore" warning for API items marked as `@internal` but whose name does not start with an underscore + +## 7.0.30 +Thu, 21 Mar 2019 01:15:32 GMT + +### Patches + +- Improve the API review file generation to include imports and support multiple exports + +## 7.0.29 +Wed, 20 Mar 2019 19:14:49 GMT + +### Patches + +- API Extractor can now analyze packages whose package.json file is missing the "version" field + +## 7.0.28 +Mon, 18 Mar 2019 04:28:43 GMT + +### Patches + +- Rename the "ae-inconsistent-release-tags" warning to "ae-different-release-tags" +- Introduce a new warning "ae-incompatible-release-tags" that checks for API signatures that reference types with incompatible release tags +- Fix an issue where this error was sometimes reported incorrectly: "The messages.extractorMessageReporting table contains an unrecognized identifier ___" + +## 7.0.27 +Fri, 15 Mar 2019 19:13:25 GMT + +### Patches + +- (Breaking change) The file extension for API review files has changed from ".api.ts" to "api.md". For details see https://github.com/microsoft/web-build-tools/issues/1123 + +## 7.0.26 +Wed, 13 Mar 2019 19:13:14 GMT + +### Patches + +- Refactor code to move the IndentedWriter API from api-extractor-model to api-documenter + +## 7.0.25 +Wed, 13 Mar 2019 01:14:05 GMT + +### Patches + +- Upgrade TSDoc + +## 7.0.24 +Mon, 11 Mar 2019 16:13:36 GMT + +### Patches + +- Fix an issue where spurious TSDoc warnings were issued because the TSDoc parser was configured improperly +- Move the .api.json related APIs into a new NPM package @microsoft/api-extractor-model + +## 7.0.23 +Tue, 05 Mar 2019 17:13:11 GMT + +### Patches + +- Issue a warning when an exported type refers to another local type that is not exported (ae-forgotten-export) +- The export analyzer now correctly handles symbols imported using "import x = require('y');" notation + +## 7.0.22 +Mon, 04 Mar 2019 17:13:19 GMT + +### Patches + +- Every error/warning message reported by API Extractor now has an associated message identifier +- Add a new section to api-extractor.json for configuring how errors get reported, with ability to suppress individual errors +- Reintroduce the ability to report issues by writing warnings into the API review file +- Fix an issue where members of type literals were incorrectly being flagged as "(undocumented)" +- Error messages now cite the original .ts source file, if a source map is present. (To enable this, specify `"declarationMap": true` in tsconfig.json.) + +## 7.0.21 +Wed, 27 Feb 2019 22:13:58 GMT + +_Version update only_ + +## 7.0.20 +Wed, 27 Feb 2019 17:13:17 GMT + +_Version update only_ + +## 7.0.19 +Mon, 18 Feb 2019 17:13:23 GMT + +### Minor changes + +- New way to resolve & generate TSDoc metadata file + +## 7.0.18 +Tue, 12 Feb 2019 17:13:12 GMT + +### Patches + +- Add a workaround for the issue where .d.ts rollups sometimes define names that conflict with a global symbol (the full solution is tracked by GitHub #1095) + +## 7.0.17 +Mon, 11 Feb 2019 10:32:37 GMT + +### Patches + +- Fix an issue where API Extractor neglected to analyze "typeof" expressions +- Fix an issue where declarations inside a namespace were sometimes being incorrectly emitted as top-level exports of the .d.ts rollup + +## 7.0.16 +Mon, 11 Feb 2019 08:55:57 GMT + +### Patches + +- Redesign the analyzer so that when an external symbol is reexported by the working package, the local object (AstImport) and external object (AstSymbol) are kept separate +- Fix a number of bugs where external symbols were misinterpreted as being part of the local project +- Eliminate a number of errors involving unusual language constructs, by avoiding analysis of external symbols unless it's really necessary +- Simplify the AstSymbol.nominalAnalysis concept and associated code +- Improve .d.ts rollup trimming to handle reexported symbols correctly + +## 7.0.15 +Mon, 11 Feb 2019 03:31:55 GMT + +### Patches + +- The `--debug` parameter now automatically breaks in the debugger when InternalError is thrown + +## 7.0.14 +Thu, 31 Jan 2019 17:03:49 GMT + +### Patches + +- Upgrade to TSDoc 0.12.5, which allows `$` in `@param` names +- Add "testMode" option in api-extractor.json to eliminate spurious diffs in test files when the version number gets bumped +- Normalize newlines for excerpt strings in the .api.json file + +## 7.0.13 +Sat, 19 Jan 2019 03:47:47 GMT + +### Patches + +- Move the skipLibCheck into the config file. + +## 7.0.12 +Sat, 19 Jan 2019 01:17:51 GMT + +### Patches + +- Fix an issue where files using "export=" were incorrectly interpreted as having ambient declarations + +## 7.0.11 +Fri, 18 Jan 2019 00:52:21 GMT + +### Patches + +- Add support for circular references between files that use `export * from "____";` + +## 7.0.10 +Thu, 17 Jan 2019 00:37:54 GMT + +### Patches + +- Add support for exports of the form `export * from "____";` +- Improve the analyzer to allow a declaration to be exported more than once +- Fix inconsistent newlines in .api.ts files + +## 7.0.9 +Thu, 10 Jan 2019 01:57:52 GMT + +### Patches + +- Fix an issue with rolling up default exports (https://github.com/microsoft/web-build-tools/issues/1007) + +## 7.0.8 +Thu, 20 Dec 2018 17:04:08 GMT + +### Patches + +- Fix an issue where it was possible to import forgotten declarations from a .d.ts rollup, even though they did not have an explicit "export" modifier + +## 7.0.7 +Wed, 19 Dec 2018 05:57:33 GMT + +### Patches + +- Extend ApiModel to support new item kinds: ApiCallSignature, ApiConstructor, ApiConstructSignature, ApiFunction, ApiIndexSignature, ApiTypeAlias, and ApiVariable + +## 7.0.6 +Fri, 14 Dec 2018 19:43:46 GMT + +### Patches + +- Update web site URLs + +## 7.0.5 +Thu, 13 Dec 2018 02:58:10 GMT + +### Patches + +- Remove unused jju dependency + +## 7.0.4 +Wed, 12 Dec 2018 17:04:19 GMT + +### Patches + +- Reintroduce support for "extends" and "implements" heritage clauses +- Redesign the Excerpt API to support multiple subranges (e.g. for a list of "implements" clauses) + +## 7.0.3 +Fri, 07 Dec 2018 17:04:56 GMT + +### Patches + +- Added more API documentation + +## 7.0.2 +Wed, 05 Dec 2018 19:57:03 GMT + +### Patches + +- fix reexported types from an external package for dts rollup + +## 7.0.1 +Wed, 05 Dec 2018 17:04:18 GMT + +### Patches + +- Fix an issue where .d.ts trimming did not properly handle variable declarations (GitHub #976) + +## 7.0.0 +Thu, 29 Nov 2018 07:02:09 GMT + +### Breaking changes + +- THIS IS A BETA RELEASE - We are bumping the version to "7.0.0" to simplify dogfooding. This release is not yet ready for general usage. + +## 6.3.0 +Wed, 28 Nov 2018 19:29:53 GMT + +### Minor changes + +- Support "extends" field in api-extractor.json config files for easier management of monorepos with many projects + +## 6.2.0 +Wed, 28 Nov 2018 02:17:11 GMT + +### Minor changes + +- Introduce a new build output "dist/tsdoc-metdata.json", which completely replaces the old "tsdocFlavor" field in package.json + +## 6.1.6 +Fri, 16 Nov 2018 21:37:10 GMT + +### Patches + +- Add support for emitting `/// ` directives in .d.ts rollups (GitHub issue #946) + +## 6.1.5 +Fri, 16 Nov 2018 00:59:00 GMT + +### Patches + +- Fix an issue where .d.ts trimming did not work for exported variable declarations (GitHub #936) + +## 6.1.4 +Fri, 09 Nov 2018 23:07:39 GMT + +### Patches + +- Upgrade to TSDoc 0.21.2, which improves trimming of link text in `@link` tags + +## 6.1.3 +Wed, 07 Nov 2018 21:04:35 GMT + +_Version update only_ + +## 6.1.2 +Mon, 05 Nov 2018 17:04:24 GMT + +### Patches + +- Upgrade to @microsoft/tsdoc 0.12.0 + +## 6.1.1 +Thu, 01 Nov 2018 19:32:52 GMT + +### Patches + +- Fix an issue where EcmaScript symbols ("computed property names") were missing from .d.ts rollups + +## 6.1.0 +Wed, 31 Oct 2018 17:00:54 GMT + +### Minor changes + +- Added an api to invoke api extractor processor by supplying api extractor json config file. + +## 6.0.9 +Thu, 25 Oct 2018 23:20:40 GMT + +_Version update only_ + +## 6.0.8 +Thu, 25 Oct 2018 08:56:02 GMT + +### Patches + +- Fix issue where `DocErrorText.text` returned `[object Object]` instead of the text + +## 6.0.7 +Wed, 24 Oct 2018 16:03:10 GMT + +_Version update only_ + +## 6.0.6 +Thu, 18 Oct 2018 01:32:20 GMT + +### Patches + +- Fix isAbsolute check for mainDtsRollupPath + +## 6.0.5 +Wed, 17 Oct 2018 21:04:49 GMT + +_Version update only_ + +## 6.0.4 +Wed, 17 Oct 2018 14:43:24 GMT + +### Patches + +- Fix a regression where namespaces were sometimes incorrectly handled in "conservative" mode +- Update the command line to look for api-extractor.json in both the "./config" folder and the project folder +- Allow type references in namespaces when namespaceSupport=conservative + +## 6.0.3 +Thu, 11 Oct 2018 23:26:07 GMT + +### Patches + +- Fix an issue where `import x from "."` was sometimes not processed correctly + +## 6.0.2 +Tue, 09 Oct 2018 06:58:01 GMT + +### Patches + +- Fix a regression where API Extractor was sometimes reporting incorrect line numbers + +## 6.0.1 +Mon, 08 Oct 2018 16:04:27 GMT + +_Version update only_ + +## 6.0.0 +Sun, 07 Oct 2018 06:15:56 GMT + +### Breaking changes + +- (Breaking change) API Extractor 6 introduces support for TSDoc doc comment syntax! Please see https://api-extractor.com/ for documentation. To learn more about the TSDoc standard, check out https://github.com/microsoft/tsdoc + +## 5.13.1 +Fri, 28 Sep 2018 16:05:35 GMT + +_Version update only_ + +## 5.13.0 +Wed, 26 Sep 2018 21:39:40 GMT + +### Minor changes + +- Add new command line option --skip-lib-check + +## 5.12.2 +Mon, 24 Sep 2018 23:06:40 GMT + +### Patches + +- Allow doc comments to use TSDoc's "@defaultvalue" tag (but the value is not yet passed to the documentation pipeline) + +## 5.12.1 +Fri, 21 Sep 2018 16:04:42 GMT + +### Patches + +- Fix an issue where TypeScript errors are often logged as "[Object object]" instead of the actual error message. + +## 5.12.0 +Thu, 20 Sep 2018 23:57:21 GMT + +### Minor changes + +- Add new feature: Support using a different version of the TypeScript compiler. + +## 5.11.2 +Tue, 18 Sep 2018 21:04:55 GMT + +### Patches + +- Fix an issue where parameters mentioned in comments were attempting to be analyzed by api-extractor. + +## 5.11.1 +Thu, 06 Sep 2018 01:25:25 GMT + +### Patches + +- Update "repository" field in package.json + +## 5.11.0 +Mon, 03 Sep 2018 16:04:45 GMT + +### Minor changes + +- Upgrade api-extractor to internally use TypeScript 3.0. + +## 5.10.8 +Wed, 29 Aug 2018 06:36:50 GMT + +_Version update only_ + +## 5.10.7 +Thu, 23 Aug 2018 18:18:53 GMT + +### Patches + +- Republish all packages in web-build-tools to resolve GitHub issue #782 + +## 5.10.6 +Wed, 22 Aug 2018 20:58:58 GMT + +_Version update only_ + +## 5.10.5 +Wed, 22 Aug 2018 16:03:25 GMT + +_Version update only_ + +## 5.10.4 +Tue, 21 Aug 2018 16:04:38 GMT + +### Patches + +- fix namespace name for export statement` + +## 5.10.3 +Thu, 09 Aug 2018 21:03:22 GMT + +_Version update only_ + +## 5.10.2 +Thu, 09 Aug 2018 16:04:24 GMT + +### Patches + +- Update lodash. + +## 5.10.1 +Thu, 26 Jul 2018 16:04:17 GMT + +_Version update only_ + +## 5.10.0 +Tue, 17 Jul 2018 16:02:52 GMT + +### Minor changes + +- Add support for new "@eventproperty" AEDoc tag, which indicates that a class/interface property should be documented as an event + +## 5.9.1 +Tue, 03 Jul 2018 21:03:31 GMT + +_Version update only_ + +## 5.9.0 +Sat, 23 Jun 2018 02:21:20 GMT + +### Minor changes + +- Add new IMarkupHtmlTag API +- AEDoc now allows HTML tags inside doc comments, which can be disabled using a backslash escape + +## 5.8.1 +Thu, 21 Jun 2018 08:27:29 GMT + +_Version update only_ + +## 5.8.0 +Tue, 19 Jun 2018 19:35:11 GMT + +### Minor changes + +- For namespaceSupport=permissive, allow arbitrary nesting of namespaces + +### Patches + +- Fix an issue where multi-line type literals sometimes had inconsistent newlines in the *.api.json file + +## 5.7.3 +Fri, 08 Jun 2018 08:43:52 GMT + +_Version update only_ + +## 5.7.2 +Thu, 31 May 2018 01:39:33 GMT + +_Version update only_ + +## 5.7.1 +Tue, 15 May 2018 02:26:45 GMT + +_Version update only_ + +## 5.7.0 +Tue, 15 May 2018 00:18:10 GMT + +### Minor changes + +- Add support for new AEDoc tags @sealed, @virtual, and @override + +## 5.6.8 +Fri, 04 May 2018 00:42:38 GMT + +### Patches + +- Fix the formatting of a log message. + +## 5.6.7 +Tue, 01 May 2018 22:03:20 GMT + +### Patches + +- Fix an issue where the *.d.ts rollup trimming did not trim import statements + +## 5.6.6 +Fri, 27 Apr 2018 03:04:32 GMT + +_Version update only_ + +## 5.6.5 +Thu, 19 Apr 2018 21:25:56 GMT + +_Version update only_ + +## 5.6.4 +Thu, 19 Apr 2018 17:02:06 GMT + +### Patches + +- Fix errors in schema documentation + +## 5.6.3 +Tue, 03 Apr 2018 16:05:29 GMT + +_Version update only_ + +## 5.6.2 +Mon, 02 Apr 2018 16:05:24 GMT + +### Patches + +- Refactor to use new @microsoft/node-core-library + +## 5.6.1 +Tue, 27 Mar 2018 01:34:25 GMT + +### Patches + +- Update build config so API Extractor builds using the latest version of itself + +## 5.6.0 +Sun, 25 Mar 2018 01:26:19 GMT + +### Minor changes + +- Improve the api-extractor.json config file so that *.d.ts rollups go in separate folders, and trimming can now be disabled + +### Patches + +- In preparation for initial release, the "Package Typings" feature was renamed to "DTS Rollup" +- Fix an issue where the @packagedocumentation comment was sometimes getting mixed into the middle of the rollup *.d.ts file + +## 5.5.2 +Fri, 23 Mar 2018 00:34:53 GMT + +### Patches + +- Upgrade colors to version ~1.2.1 + +## 5.5.1 +Tue, 20 Mar 2018 02:44:45 GMT + +### Patches + +- Improve packageTypings generator to trim nested members according to their release tag +- Fix a bug where packageTypings failed to handle merged declarations properly + +## 5.5.0 +Sat, 17 Mar 2018 02:54:22 GMT + +### Minor changes + +- Overhaul the packageTypings generator analysis to get ready for the upcoming nested member trimming +- Breaking change: Any projects using the package typings feature must now have a "tsdoc" section in their package.json + +### Patches + +- Add "--debug" flag for debugging + +## 5.4.0 +Thu, 15 Mar 2018 20:00:50 GMT + +### Minor changes + +- Add a new setting validationRules.missingReleaseTags to optionally remove the requirement that every API item should have a release tag +- Add new API "Markup.formatApiItemReference()" + +### Patches + +- Fix an issue where the automatically generated documentation for class constructors sometimes had a broken hyperlink + +## 5.3.9 +Thu, 15 Mar 2018 16:05:43 GMT + +_Version update only_ + +## 5.3.8 +Mon, 12 Mar 2018 20:36:19 GMT + +### Patches + +- Locked down some "@types/" dependency versions to avoid upgrade conflicts + +## 5.3.7 +Tue, 06 Mar 2018 17:04:51 GMT + +### Patches + +- Add preliminary support for preview and public outputs for packageTypings generator + +## 5.3.6 +Fri, 02 Mar 2018 01:13:59 GMT + +_Version update only_ + +## 5.3.5 +Tue, 27 Feb 2018 22:05:57 GMT + +_Version update only_ + +## 5.3.4 +Wed, 21 Feb 2018 22:04:19 GMT + +_Version update only_ + +## 5.3.3 +Wed, 21 Feb 2018 03:13:28 GMT + +_Version update only_ + +## 5.3.2 +Sat, 17 Feb 2018 02:53:49 GMT + +### Patches + +- Fix several bugs with the way that imports were being deduplicated by the packageTypings feature + +## 5.3.1 +Fri, 16 Feb 2018 22:05:23 GMT + +_Version update only_ + +## 5.3.0 +Fri, 16 Feb 2018 17:05:11 GMT + +### Minor changes + +- Fix an issue where the packageTypings feature didn't handle some import/export patterns + +### Patches + +- Fix an issue where the packageTypings feature sometimes emitted "default" instead of the class name +- Improve the packageTypings feature to support triple-slash references to typings + +## 5.2.7 +Wed, 07 Feb 2018 17:05:11 GMT + +_Version update only_ + +## 5.2.6 +Fri, 26 Jan 2018 22:05:30 GMT + +_Version update only_ + +## 5.2.5 +Fri, 26 Jan 2018 17:53:38 GMT + +### Patches + +- Force a patch bump in case the previous version was an empty package + +## 5.2.4 +Fri, 26 Jan 2018 00:36:51 GMT + +_Version update only_ + +## 5.2.3 +Tue, 23 Jan 2018 17:05:28 GMT + +_Version update only_ + +## 5.2.2 +Thu, 18 Jan 2018 03:23:46 GMT + +### Patches + +- Enable package typings generated by api-extractor + +## 5.2.1 +Thu, 18 Jan 2018 00:48:06 GMT + +_Version update only_ + +## 5.2.0 +Thu, 18 Jan 2018 00:27:23 GMT + +### Minor changes + +- Improve the packageTypings feature to support abstract classes and "import * as X" imports + +## 5.1.3 +Wed, 17 Jan 2018 10:49:31 GMT + +_Version update only_ + +## 5.1.2 +Fri, 12 Jan 2018 03:35:22 GMT + +### Patches + +- Add some incremental improvements for the experimental PackageTypingsGenerator feature + +## 5.1.1 +Thu, 11 Jan 2018 22:31:51 GMT + +_Version update only_ + +## 5.1.0 +Wed, 10 Jan 2018 20:40:01 GMT + +### Minor changes + +- Upgrade to Node 8 + +### Patches + +- Continued progress for the experimental PackageTypingsGenerator + +## 5.0.1 +Tue, 09 Jan 2018 17:05:51 GMT + +### Patches + +- Get web-build-tools building with pnpm + +## 5.0.0 +Sun, 07 Jan 2018 05:12:08 GMT + +### Breaking changes + +- API Extractor now processes *.d.ts files instead of *.ts files + +### Minor changes + +- Introduced new tag @packagedocumentation which replaces the earlier approach that used a "packageDescription" variable + +## 4.3.7 +Fri, 05 Jan 2018 20:26:45 GMT + +_Version update only_ + +## 4.3.6 +Fri, 05 Jan 2018 00:48:41 GMT + +### Patches + +- Update Jest to ~21.2.1 + +## 4.3.5 +Fri, 22 Dec 2017 17:04:46 GMT + +### Patches + +- Fixed an issue where warnings would cause the api-extractor tool to return a nonzero exit code for a "--local" build; warnings should not fail the build in this scenario + +## 4.3.4 +Tue, 12 Dec 2017 03:33:26 GMT + +_Version update only_ + +## 4.3.3 +Thu, 30 Nov 2017 23:59:09 GMT + +_Version update only_ + +## 4.3.2 +Thu, 30 Nov 2017 23:12:21 GMT + +_Version update only_ + +## 4.3.1 +Wed, 29 Nov 2017 17:05:37 GMT + +_Version update only_ + +## 4.3.0 +Tue, 28 Nov 2017 23:43:55 GMT + +### Minor changes + +- Add Extractor.processProject() whose return value indicates success + +### Patches + +- Deprecate Extractor.analyzeProject() API + +## 4.2.6 +Mon, 13 Nov 2017 17:04:50 GMT + +_Version update only_ + +## 4.2.5 +Mon, 06 Nov 2017 17:04:18 GMT + +_Version update only_ + +## 4.2.4 +Thu, 02 Nov 2017 16:05:24 GMT + +### Patches + +- lock the reference version between web build tools projects + +## 4.2.3 +Wed, 01 Nov 2017 21:06:08 GMT + +### Patches + +- Upgrade cyclic dependencies + +## 4.2.2 +Tue, 31 Oct 2017 21:04:04 GMT + +_Version update only_ + +## 4.2.1 +Tue, 31 Oct 2017 16:04:55 GMT + +_Version update only_ + +## 4.2.0 +Wed, 25 Oct 2017 20:03:59 GMT + +### Minor changes + +- Improved the way API JSON represents documentation markup; this is a file format change + +## 4.1.2 +Tue, 24 Oct 2017 18:17:12 GMT + +_Version update only_ + +## 4.1.1 +Mon, 23 Oct 2017 21:53:12 GMT + +### Patches + +- Updated cyclic dependencies + +## 4.1.0 +Fri, 20 Oct 2017 19:57:12 GMT + +### Minor changes + +- Add policies.namespaceSupport option to API Extractor config + +### Patches + +- Fixed an issue where properties were sometimes marked as readonly; a remark is automatically generated for classes with internal constructors + +## 4.0.1 +Fri, 20 Oct 2017 01:52:54 GMT + +### Patches + +- Rename ApiExtractor class to Extractor + +## 4.0.0 +Fri, 20 Oct 2017 01:04:44 GMT + +### Breaking changes + +- Redesigned interface for invoking API Extractor + +## 3.4.2 +Thu, 05 Oct 2017 01:05:02 GMT + +_Version update only_ + +## 3.4.1 +Fri, 29 Sep 2017 01:03:42 GMT + +### Patches + +- Removed IMarkupPage.docId + +## 3.4.0 +Thu, 28 Sep 2017 01:04:28 GMT + +### Minor changes + +- Skipping two lines in an AEDoc comment now creates a paragraph separator for the generated documentation + +### Patches + +- The *.api.json "linkDocElement" type now always explicitly specifies the package name, rather than expecting the reader to infer it +- The *.api.json file format now exposes "signature" information for properties, functions, and module variables + +## 3.3.0 +Fri, 22 Sep 2017 01:04:02 GMT + +### Minor changes + +- Upgrade to es6 + +## 3.2.6 +Wed, 20 Sep 2017 22:10:17 GMT + +_Version update only_ + +## 3.2.5 +Mon, 11 Sep 2017 13:04:55 GMT + +### Patches + +- The isBeta and deprecatedMessage fields are now inherited in the *.api.json files +- Fix an issue where the *.api.json file was sometimes missing function parameters + +## 3.2.4 +Fri, 08 Sep 2017 01:28:04 GMT + +### Patches + +- Deprecate @types/es6-coll ections in favor of built-in typescript typings 'es2015.collection' a nd 'es2015.iterable' + +## 3.2.3 +Thu, 07 Sep 2017 13:04:35 GMT + +### Patches + +- Fix incorrect schema/typings for enum members + +## 3.2.2 +Thu, 07 Sep 2017 00:11:11 GMT + +### Patches + +- Add $schema field to all schemas + +## 3.2.1 +Wed, 06 Sep 2017 13:03:42 GMT + +### Patches + +- Converted IMarkupDocumentationLink to IMarkupApiLink, which exposes the underlying IApiItemReference rather than assuming a particular "document ID" model + +## 3.2.0 +Tue, 05 Sep 2017 19:03:56 GMT + +### Minor changes + +- Add the constructor signature and package name to the exported API signature + +## 3.1.0 +Sat, 02 Sep 2017 01:04:26 GMT + +### Minor changes + +- Expanded the api-extractor API to expose interfaces for the *.api.json file fileformat + +## 3.0.0 +Thu, 31 Aug 2017 18:41:18 GMT + +### Breaking changes + +- Fix compatibility issues with old releases, by incrementing the major version number + +## 2.3.7 +Thu, 31 Aug 2017 17:46:25 GMT + +### Patches + +- Fix issue where node-core-library was not an explicit dependency + +## 2.3.6 +Wed, 30 Aug 2017 01:04:34 GMT + +_Version update only_ + +## 2.3.5 +Thu, 24 Aug 2017 22:44:12 GMT + +### Patches + +- Update the schema validator. + +## 2.3.4 +Thu, 24 Aug 2017 01:04:33 GMT + +_Version update only_ + +## 2.3.3 +Tue, 22 Aug 2017 13:04:22 GMT + +### Patches + +- Added "api-documenter" code sample + +## 2.3.2 +Tue, 15 Aug 2017 01:29:31 GMT + +### Patches + +- Introduce Span parser for upcoming *.d.ts generator + +## 2.3.1 +Thu, 27 Jul 2017 01:04:48 GMT + +### Patches + +- Upgrade to the TS2.4 version of the build tools. + +## 2.3.0 +Tue, 25 Jul 2017 20:03:31 GMT + +### Minor changes + +- Upgrade to TypeScript 2.4 + +## 2.2.0 +Wed, 21 Jun 2017 04:19:35 GMT + +### Minor changes + +- Add two new features: An error is reported if a top-level definition is missing its release tag. The constructor summary will now be autogenerated if omitted. + +## 2.0.10 +Tue, 20 Jun 2017 01:04:54 GMT + +### Patches + +- Improve the wording of many error messages +- Fix a bug with parsing of @link tags +- Issue warnings for @internal definitions that are not prefixed with an underscore + +## 2.0.9 +Sat, 17 Jun 2017 01:02:59 GMT + +### Patches + +- The unsupported @summary tag is now reported as an error +- Use a cache to speed up package.json lookups + +## 2.0.8 +Wed, 14 Jun 2017 13:03:40 GMT + +### Patches + +- Definitions marked as @beta are now included in the *.api.json files for documentation + +## 2.0.7 +Thu, 08 Jun 2017 05:15:52 GMT + +### Patches + +- Updated README.md + +## 2.0.6 +Mon, 15 May 2017 21:59:43 GMT + +### Patches + +- Added support for Namespace with ApiNamespace + +## 2.0.5 +Sat, 22 Apr 2017 01:02:03 GMT + +### Patches + +- Added check for API names that are not supported (only letters and numbers supported) + +## 2.0.4 +Wed, 19 Apr 2017 20:18:06 GMT + +### Patches + +- Remove ES6 Promise & @types/es6-promise typings + +## 2.0.3 +Fri, 14 Apr 2017 17:44:08 GMT + +### Patches + +- Added collect references ability to detect determine type information of return types and parameter types. + +## 2.0.2 +Fri, 07 Apr 2017 21:43:16 GMT + +### Patches + +- Adjusted the version specifier for typescript to ~2.2.2 + +## 2.0.1 +Thu, 06 Apr 2017 01:32:23 GMT + +### Patches + +- Removed hard coding of @public for ApiPackage + +## 2.0.0 +Mon, 20 Mar 2017 21:52:20 GMT + +### Breaking changes + +- Fixing whitespace, also a variable that was shadowing another variable. + +## 1.1.19 +Mon, 20 Mar 2017 04:20:13 GMT + +### Patches + +- Reverting change. + +## 1.1.18 +Mon, 20 Mar 2017 03:50:55 GMT + +### Patches + +- Reverting previous change, which causes a regression in SPFx yeoman sc enario. + +## 1.1.17 +Mon, 20 Mar 2017 00:54:03 GMT + +### Patches + +- Fixing lint whitespace issues. + +## 1.1.16 +Sun, 19 Mar 2017 19:10:30 GMT + +### Patches + +- Fixing variable that was shadowing another variable. + +## 1.1.15 +Wed, 15 Mar 2017 01:32:09 GMT + +### Patches + +- Locking `@types` packages. Synchronizing version specifiers for dependencies with other `web-build-tools` projects. + +## 1.1.14 +Sat, 18 Feb 2017 02:32:06 GMT + +### Patches + +- Seperated the ApiItem initialization into 3 stages: create documentation that doesn't require resolution, then complete initialization by resolving links and inheritdocs. This allows us to ignore harmless cycles like type references" + +## 1.1.13 +Thu, 16 Feb 2017 22:10:39 GMT + +### Patches + +- Fixed Api-Extractor error message, changed apostrophe to backtick. + +## 1.1.12 +Thu, 16 Feb 2017 18:56:57 GMT + +### Patches + +- Added support for local API definition resolution" + +## 1.1.11 +Sat, 11 Feb 2017 02:32:35 GMT + +### Patches + +- Changed dependency for ApiDocumentation to abstract the resolving of API definition references. + +## 1.1.10 +Fri, 10 Feb 2017 20:01:30 GMT + +### Patches + +- Added support to not throw error, instead report error if no type is declared on properties and parameters + +## 1.1.9 +Tue, 07 Feb 2017 20:37:06 GMT + +### Patches + +- Fixing issue where undocumented comment was not being emitted. + +## 1.1.8 +Sat, 04 Feb 2017 02:32:05 GMT + +### Patches + +- Moved ApiItem references within ApiDocumentation, to ApiItem caller. + +## 1.1.7 +Thu, 02 Feb 2017 14:05:53 GMT + +### Patches + +- Refactored ApiDocumentation creation to resolve references method. + +## 1.1.6 +Wed, 01 Feb 2017 20:09:30 GMT + +### Patches + +- Added ApiItemKind enum and refactored child classes. + +## 1.1.5 +Fri, 27 Jan 2017 20:04:15 GMT + +### Patches + +- Changed name of Analyzer to Extractor, added support for external api json doc loading. + +## 1.1.4 +Fri, 27 Jan 2017 02:35:10 GMT + +### Patches + +- Added ExternalApiHelper class to be used in generating api documentation json files for external types. +- Added description for packages implementation. +- Added config folder with file to enable api-extractor on itself. rebuild project on previous build. + +## 1.1.3 +Tue, 24 Jan 2017 01:36:35 GMT + +### Patches + +- Json schema was updated to reflect feature additions to linkDocElement. The linkDocElement can now be of type 'code' which refers to an API definition reference. + +## 1.1.2 +Fri, 20 Jan 2017 01:46:41 GMT + +_Version update only_ + +## 1.1.1 +Thu, 19 Jan 2017 20:04:40 GMT + +### Patches + +- Check for missing JSDoc sequences changed. +- Improved error messages + +## 1.1.0 +Wed, 18 Jan 2017 20:04:29 GMT + +### Minor changes + +- Updating API Extractor to work with TypeScript 2.1 + +## 1.0.2 +Mon, 16 Jan 2017 20:04:15 GMT + +### Patches + +- @link capability for href and API definition references + +## 1.0.1 +Fri, 13 Jan 2017 06:46:05 GMT + +_Version update only_ + +## 1.0.0 +Wed, 11 Jan 2017 14:11:26 GMT + +### Breaking changes + +- Introducing API Extractor + diff --git a/packages/api-extractor/LICENSE b/packages/api-extractor/LICENSE new file mode 100644 index 000000000..d9cfa3eb0 --- /dev/null +++ b/packages/api-extractor/LICENSE @@ -0,0 +1,24 @@ +@microsoft/api-extractor + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/api-extractor/README.md b/packages/api-extractor/README.md new file mode 100644 index 000000000..734576b9e --- /dev/null +++ b/packages/api-extractor/README.md @@ -0,0 +1,49 @@ +# @microsoft/api-extractor + +![API Extractor](https://github.com/microsoft/rushstack/raw/main/common/wiki-images/api-extractor-title.png?raw=true) +
+      https://api-extractor.com/ + + + + + +**API Extractor** helps you build better [TypeScript](https://www.typescriptlang.org/) library packages. Suppose for example that your company has published an NPM package called "**awesome-widgets**" that exports many classes and interfaces. As developers start to depend on your library, you may encounter issues such as... + +- **Accidental breaks:** People keep reporting that their code won't compile after a supposedly "minor" update. To address this, you boldly propose that every **awesome-widgets** pull request must be approved by an experienced developer from your team. But that proves unrealistic -- nobody has time to look at every single PR! What you really need is a way to detect PRs that change API contracts, and flag them for review. That would focus attention in the right place... but how to do that? + +- **Missing exports:** Suppose the **awesome-widgets** package exports an API function `AwesomeButton.draw()` that requires a parameter of type `DrawStyle`, but you forgot to export this enum. Things seem fine at first, but when a developer tries to call that function, they discover that there's no way to specify the `DrawStyle`. How to avoid these oversights? + +- **Accidental exports:** You meant for your `DrawHelper` class to be kept internal, but one day you realize it's being exported. When you try to remove it, consumers complain that they're using it. How do we avoid this in the future? + +- **Alpha/Beta graduation:** You want to release previews of new APIs that are not ready for prime time yet. But if you did a major SemVer bump every time these definitions evolve, the villagers would be after you with torches and pitchforks! A better approach is to designate certain classes/members as **alpha** quality, then promote them to **beta** and finally to **public** as they mature. But how to indicate this to your consumers? (And how to detect scoping mistakes? A **public** function should never return a **beta** result.) + +- **\*.d.ts rollup:** You webpacked your library into a nice **\*.js** bundle file -- so why ship your typings as a messy tree of **lib/\*.d.ts** files full of private definitions? Can't we consolidate them into a tidy **\*.d.ts** rollup file? And if you publish internal/beta/public releases, each release type should get its own **\*.d.ts** file with appropriate trimming. Developers building a production project don't want to see a bunch of **internal** and **beta** members in their VS Code IntelliSense! + +- **Online documentation:** You have faithfully annotated each TypeScript member with nice [TSDoc](https://github.com/microsoft/tsdoc) descriptions. Now that your library has shipped, it's time to set up [a nicely formatted](https://docs.microsoft.com/en-us/javascript/api/sp-http) API reference. What tool to use? + +**API Extractor** provides an integrated, professional-quality solution for all these problems. It is invoked at build time by your toolchain and leverages the TypeScript compiler engine to: + +- Detect a project's exported API surface +- Capture the contracts in a concise report designed to facilitate review +- Warn about common mistakes (e.g. missing exports, inconsistent visibility, etc.) +- Generate \*.d.ts rollups with trimming according to release type +- Output API documentation in a portable format that's easy to integrate with your content pipeline + +Best of all, **API Extractor** is free and open source. Join the community and create a pull request! + + + + + +## Getting Started + +For more details and support resources, please visit: https://api-extractor.com/ + +## Links + +- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - Find + out what's new in the latest version +- [API Reference](https://rushstack.io/pages/api/api-extractor/) + +API Extractor is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/packages/api-extractor/bin/api-extractor b/packages/api-extractor/bin/api-extractor new file mode 100755 index 000000000..662e6e9d9 --- /dev/null +++ b/packages/api-extractor/bin/api-extractor @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../dist/start.js'); diff --git a/packages/api-extractor/config/api-extractor.json b/packages/api-extractor/config/api-extractor.json new file mode 100644 index 000000000..c4d10dfdc --- /dev/null +++ b/packages/api-extractor/config/api-extractor.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "../../../common/temp/api/.api.json" + }, + + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "/dist/rollup.d.ts" + } +} diff --git a/packages/api-extractor/config/jest.config.json b/packages/api-extractor/config/jest.config.json new file mode 100644 index 000000000..f1f64e2bb --- /dev/null +++ b/packages/api-extractor/config/jest.config.json @@ -0,0 +1,64 @@ +{ + // By default, don't hide console output + "silent": false, + + // In order for HeftJestReporter to receive console.log() events, we must set verbose=false + "verbose": false, + + // If mocks are not cleared between tests, it opens the door to accidental reliance on + // ordering of tests or describe blocks, eventually resulting in intermittent failures. + // + // We suggest this setting for any heft project (in a monorepo or not). + "clearMocks": true, + + // "Adding '/dist' here enables dist/__mocks__ to be used for mocking Node.js system modules + "roots": ["/dist"], + + "testMatch": ["/dist/**/*.test.{cjs,js}"], + "testPathIgnorePatterns": ["/node_modules/"], + + "collectCoverageFrom": [ + "dist/**/*.{cjs,js}", + "!dist/**/*.d.ts", + "!dist/**/*.test.{cjs,js}", + "!dist/**/test/**", + "!dist/**/__tests__/**", + "!dist/**/__fixtures__/**", + "!dist/**/__mocks__/**" + ], + "coveragePathIgnorePatterns": ["/node_modules/"], + + "testEnvironment": "jest-environment-node", + + "testEnvironmentOptions": { + "url": "http://localhost/", + "customExportConditions": ["require", "node"] + }, + + // Retain pre-Jest 29 snapshot behavior + "snapshotFormat": { + "escapeString": true, + "printBasicPrototype": true + }, + + // Instruct jest not to run the transformer pipeline by default on JS files. The output files from TypeScript + // will already be fully transformed, so this avoids redundant file system operations. + "transformIgnorePatterns": ["\\.c?js$"], + + // The modulePathIgnorePatterns below accepts these sorts of paths: + // - /dist + // - /dist/file.js + // ...and ignores anything else under + "modulePathIgnorePatterns": [], + + // Prefer .cjs to .js to catch explicit commonjs output. Optimize for local files, which will be .js + "moduleFileExtensions": ["cjs", "js", "json", "node"], + + // Enable code coverage for Jest + "collectCoverage": true, + "coverageDirectory": "/coverage", + "coverageReporters": ["cobertura", "html"], + + // Use v8 coverage provider to avoid Babel + "coverageProvider": "v8" +} diff --git a/packages/api-extractor/extends/tsdoc-base.json b/packages/api-extractor/extends/tsdoc-base.json new file mode 100644 index 000000000..119244c55 --- /dev/null +++ b/packages/api-extractor/extends/tsdoc-base.json @@ -0,0 +1,72 @@ +/** + * This file defines the TSDoc custom tags for use with API Extractor. + * + * If your project has a custom tsdoc.json file, then it should use the "extends" field to + * inherit the definitions from this file. For example: + * + * ``` + * { + * "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + * "extends": [ "@microsoft/api-extractor/extends/tsdoc-config.json" ], + * . . . + * } + * ``` + * + * For details about this config file, please see: https://tsdoc.org/pages/packages/tsdoc-config/ + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + /** + * The "AEDoc" custom tags: + */ + "tagDefinitions": [ + { + "tagName": "@betaDocumentation", + "syntaxKind": "modifier" + }, + { + "tagName": "@internalRemarks", + "syntaxKind": "block" + }, + { + "tagName": "@preapproved", + "syntaxKind": "modifier" + } + ], + + /** + * TSDoc tags implemented by API Extractor: + */ + "supportForTags": { + "@alpha": true, + "@beta": true, + "@defaultValue": true, + "@decorator": true, + "@deprecated": true, + "@eventProperty": true, + "@example": true, + "@experimental": true, + "@inheritDoc": true, + "@internal": true, + "@label": true, + "@link": true, + "@override": true, + "@packageDocumentation": true, + "@param": true, + "@privateRemarks": true, + "@public": true, + "@readonly": true, + "@remarks": true, + "@returns": true, + "@sealed": true, + "@see": true, + "@throws": true, + "@typeParam": true, + "@virtual": true, + + "@betaDocumentation": true, + "@internalRemarks": true, + "@preapproved": true + } +} diff --git a/packages/api-extractor/package.json b/packages/api-extractor/package.json new file mode 100644 index 000000000..d9b05f9e6 --- /dev/null +++ b/packages/api-extractor/package.json @@ -0,0 +1,81 @@ +{ + "name": "@discordjs/api-extractor", + "version": "7.38.1", + "description": "Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups", + "private": true, + "keywords": [ + "typescript", + "API", + "JSDoc", + "AEDoc", + "TSDoc", + "generate", + "documentation", + "declaration", + "dts", + ".d.ts", + "rollup", + "bundle", + "compiler", + "alpha", + "beta" + ], + "repository": { + "type": "git", + "url": "https://github.com/discordjs/discord.js.git", + "directory": "packages/api-extractor" + }, + "homepage": "https://discord.js.org", + "bin": { + "api-extractor": "./bin/api-extractor" + }, + "license": "MIT", + "scripts": { + "build": "tsc --noEmit && tsup && pnpm run copy-files", + "copy-files": "cpy 'src/schemas/*.json' 'dist/schemas' && cpy 'extends/*.json' 'dist/extends'", + "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src", + "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src" + }, + "exports": { + ".": { + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@discordjs/api-extractor-model": "workspace:^", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "0.16.2", + "@rushstack/node-core-library": "3.61.0", + "@rushstack/rig-package": "0.5.1", + "@rushstack/ts-command-line": "4.17.1", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "^5.2.2" + }, + "devDependencies": { + "@types/lodash": "^4.14.200", + "@types/node": "^18.18.8", + "@types/resolve": "^1.20.4", + "@types/semver": "^7.5.0", + "@types/jest": "^29.5.7", + "cpy-cli": "^5.0.0", + "cross-env": "^7.0.3", + "eslint": "^8.53.0", + "eslint-config-neon": "^0.1.57", + "eslint-formatter-pretty": "^5.0.0", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "tsup": "^7.2.0", + "turbo": "^1.10.16" + } +} diff --git a/packages/api-extractor/src/aedoc/PackageDocComment.ts b/packages/api-extractor/src/aedoc/PackageDocComment.ts new file mode 100644 index 000000000..5fd575bb5 --- /dev/null +++ b/packages/api-extractor/src/aedoc/PackageDocComment.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; +import { ExtractorMessageId } from '../api/ExtractorMessageId.js'; +import type { Collector } from '../collector/Collector.js'; + +export class PackageDocComment { + /** + * For the given source file, see if it starts with a TSDoc comment containing the `@packageDocumentation` tag. + */ + public static tryFindInSourceFile(sourceFile: ts.SourceFile, collector: Collector): ts.TextRange | undefined { + // The @packageDocumentation comment is special because it is not attached to an AST + // definition. Instead, it is part of the "trivia" tokens that the compiler treats + // as irrelevant white space. + // + // WARNING: If the comment doesn't precede an export statement, the compiler will omit + // it from the *.d.ts file, and API Extractor won't find it. If this happens, you need + // to rearrange your statements to ensure it is passed through. + // + // This implementation assumes that the "@packageDocumentation" will be in the first TSDoc comment + // that appears in the entry point *.d.ts file. We could possibly look in other places, + // but the above warning suggests enforcing a standardized layout. This design choice is open + // to feedback. + let packageCommentRange: ts.TextRange | undefined; // empty string + + for (const commentRange of ts.getLeadingCommentRanges(sourceFile.text, sourceFile.getFullStart()) ?? []) { + if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) { + const commentBody: string = sourceFile.text.slice(commentRange.pos, commentRange.end); + + // Choose the first JSDoc-style comment + if (/^\s*\/\*\*/.test(commentBody)) { + // But only if it looks like it's trying to be @packageDocumentation + // (The TSDoc parser will validate this more rigorously) + if (/@packagedocumentation/i.test(commentBody)) { + packageCommentRange = commentRange; + } + + break; + } + } + } + + if (!packageCommentRange) { + // If we didn't find the @packageDocumentation tag in the expected place, is it in some + // wrong place? This sanity check helps people to figure out why there comment isn't working. + for (const statement of sourceFile.statements) { + const ranges: ts.CommentRange[] = []; + ranges.push(...(ts.getLeadingCommentRanges(sourceFile.text, statement.getFullStart()) ?? [])); + ranges.push(...(ts.getTrailingCommentRanges(sourceFile.text, statement.getEnd()) ?? [])); + + for (const commentRange of ranges) { + const commentBody: string = sourceFile.text.slice(commentRange.pos, commentRange.end); + + if (/@packagedocumentation/i.test(commentBody)) { + collector.messageRouter.addAnalyzerIssueForPosition( + ExtractorMessageId.MisplacedPackageTag, + 'The @packageDocumentation comment must appear at the top of entry point *.d.ts file', + sourceFile, + commentRange.pos, + ); + break; + } + } + } + } + + return packageCommentRange; + } +} diff --git a/packages/api-extractor/src/analyzer/AstDeclaration.ts b/packages/api-extractor/src/analyzer/AstDeclaration.ts new file mode 100644 index 000000000..3b7bcc4b6 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstDeclaration.ts @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import type { AstEntity } from './AstEntity.js'; +import type { AstSymbol } from './AstSymbol.js'; +import { Span } from './Span.js'; + +/** + * Constructor options for AstDeclaration + */ +export interface IAstDeclarationOptions { + readonly astSymbol: AstSymbol; + readonly declaration: ts.Declaration; + readonly parent: AstDeclaration | undefined; +} + +/** + * The AstDeclaration and AstSymbol classes are API Extractor's equivalent of the compiler's + * ts.Declaration and ts.Symbol objects. They are created by the `AstSymbolTable` class. + * + * @remarks + * The AstDeclaration represents one or more syntax components of a symbol. Usually there is + * only one AstDeclaration per AstSymbol, but certain TypeScript constructs can have multiple + * declarations (e.g. overloaded functions, merged declarations, etc.). + * + * Because of this, the `AstDeclaration` manages the parent/child nesting hierarchy (e.g. with + * declaration merging, each declaration has its own children) and becomes the main focus + * of analyzing AEDoc and emitting *.d.ts files. + * + * The AstDeclarations correspond to items from the compiler's ts.Node hierarchy, but + * omitting/skipping any nodes that don't match the AstDeclaration.isSupportedSyntaxKind() + * criteria. This simplification makes the other API Extractor stages easier to implement. + */ +export class AstDeclaration { + public readonly declaration: ts.Declaration; + + public readonly astSymbol: AstSymbol; + + /** + * The parent, if this object is nested inside another AstDeclaration. + */ + public readonly parent: AstDeclaration | undefined; + + /** + * A bit set of TypeScript modifiers such as "private", "protected", etc. + */ + public readonly modifierFlags: ts.ModifierFlags; + + /** + * Additional information that is calculated later by the `Collector`. The actual type is `DeclarationMetadata`, + * but we declare it as `unknown` because consumers must obtain this object by calling + * `Collector.fetchDeclarationMetadata()`. + */ + public declarationMetadata: unknown; + + /** + * Additional information that is calculated later by the `Collector`. The actual type is `ApiItemMetadata`, + * but we declare it as `unknown` because consumers must obtain this object by calling + * `Collector.fetchApiItemMetadata()`. + */ + public apiItemMetadata: unknown; + + // NOTE: This array becomes immutable after astSymbol.analyze() sets astSymbol.analyzed=true + private readonly _analyzedChildren: AstDeclaration[] = []; + + private readonly _analyzedReferencedAstEntitiesSet: Set = new Set(); + + // Reverse lookup used by findChildrenWithName() + private _childrenByName: Map | undefined = undefined; + + public constructor(options: IAstDeclarationOptions) { + this.declaration = options.declaration; + this.astSymbol = options.astSymbol; + this.parent = options.parent; + + this.astSymbol._notifyDeclarationAttach(this); + + if (this.parent) { + this.parent._notifyChildAttach(this); + } + + this.modifierFlags = ts.getCombinedModifierFlags(this.declaration); + + // Check for ECMAScript private fields, for example: + // + // class Person { #name: string; } + // + const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(this.declaration); + if (declarationName && ts.isPrivateIdentifier(declarationName)) { + this.modifierFlags |= ts.ModifierFlags.Private; + } + } + + /** + * Returns the children for this AstDeclaration. + * + * @remarks + * The collection will be empty until AstSymbol.analyzed is true. + */ + public get children(): readonly AstDeclaration[] { + return this.astSymbol.analyzed ? this._analyzedChildren : []; + } + + /** + * Returns the AstEntity objects referenced by this node. + * + * @remarks + * NOTE: The collection will be empty until AstSymbol.analyzed is true. + * + * Since we assume references are always collected by a traversal starting at the + * root of the nesting declarations, this array omits the following items because they + * would be redundant: + * - symbols corresponding to parents of this declaration (e.g. a method that returns its own class) + * - symbols already listed in the referencedAstSymbols property for parents of this declaration + * (e.g. a method that returns its own class's base class) + * - symbols that are referenced only by nested children of this declaration + * (e.g. if a method returns an enum, this doesn't imply that the method's class references that enum) + */ + public get referencedAstEntities(): readonly AstEntity[] { + return this.astSymbol.analyzed ? [...this._analyzedReferencedAstEntitiesSet] : []; + } + + /** + * This is an internal callback used when the AstSymbolTable attaches a new + * child AstDeclaration to this object. + * + * @internal + */ + public _notifyChildAttach(child: AstDeclaration): void { + if (child.parent !== this) { + throw new InternalError('Invalid call to notifyChildAttach()'); + } + + if (this.astSymbol.analyzed) { + throw new InternalError('_notifyChildAttach() called after analysis is already complete'); + } + + this._analyzedChildren.push(child); + } + + /** + * Returns a diagnostic dump of the tree, which reports the hierarchy of + * AstDefinition objects. + */ + public getDump(indent: string = ''): string { + const declarationKind: string = ts.SyntaxKind[this.declaration.kind]; + let result: string = indent + `+ ${this.astSymbol.localName} (${declarationKind})`; + if (this.astSymbol.nominalAnalysis) { + result += ' (nominal)'; + } + + result += '\n'; + + for (const referencedAstEntity of this._analyzedReferencedAstEntitiesSet.values()) { + result += indent + ` ref: ${referencedAstEntity.localName}\n`; + } + + for (const child of this.children) { + result += child.getDump(indent + ' '); + } + + return result; + } + + /** + * Returns a diagnostic dump using Span.getDump(), which reports the detailed + * compiler structure. + */ + public getSpanDump(indent: string = ''): string { + const span: Span = new Span(this.declaration); + return span.getDump(indent); + } + + /** + * This is an internal callback used when AstSymbolTable.analyze() discovers a new + * type reference associated with this declaration. + * + * @internal + */ + public _notifyReferencedAstEntity(referencedAstEntity: AstEntity): void { + if (this.astSymbol.analyzed) { + throw new InternalError('_notifyReferencedAstEntity() called after analysis is already complete'); + } + + for (let current: AstDeclaration | undefined = this; current; current = current.parent) { + // Don't add references to symbols that are already referenced by a parent + if (current._analyzedReferencedAstEntitiesSet.has(referencedAstEntity)) { + return; + } + + // Don't add the symbols of parents either + if (referencedAstEntity === current.astSymbol) { + return; + } + } + + this._analyzedReferencedAstEntitiesSet.add(referencedAstEntity); + } + + /** + * Visits all the current declaration and all children recursively in a depth-first traversal, + * and performs the specified action for each one. + */ + public forEachDeclarationRecursive(action: (astDeclaration: AstDeclaration) => void): void { + action(this); + for (const child of this.children) { + child.forEachDeclarationRecursive(action); + } + } + + /** + * Returns the list of child declarations whose `AstSymbol.localName` matches the provided `name`. + * + * @remarks + * This is an efficient O(1) lookup. + */ + public findChildrenWithName(name: string): readonly AstDeclaration[] { + // The children property returns: + // + // return this.astSymbol.analyzed ? this._analyzedChildren : []; + // + if (!this.astSymbol.analyzed || this._analyzedChildren.length === 0) { + return []; + } + + if (this._childrenByName === undefined) { + // Build the lookup table + const childrenByName: Map = new Map(); + + for (const child of this._analyzedChildren) { + const childName: string = child.astSymbol.localName; + let array: AstDeclaration[] | undefined = childrenByName.get(childName); + if (array === undefined) { + array = []; + childrenByName.set(childName, array); + } + + array.push(child); + } + + this._childrenByName = childrenByName; + } + + return this._childrenByName.get(name) ?? []; + } + + /** + * This function determines which ts.Node kinds will generate an AstDeclaration. + * These correspond to the definitions that we can add AEDoc to. + */ + public static isSupportedSyntaxKind(kind: ts.SyntaxKind): boolean { + // (alphabetical order) + switch (kind) { + case ts.SyntaxKind.CallSignature: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ConstructSignature: // Example: "new(x: number): IMyClass" + case ts.SyntaxKind.Constructor: // Example: "constructor(x: number)" + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.FunctionDeclaration: // Example: "(x: number): number" + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.IndexSignature: // Example: "[key: string]: string" + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.ModuleDeclaration: // Used for both "module" and "namespace" declarations + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.TypeAliasDeclaration: // Example: "type Shape = Circle | Square" + case ts.SyntaxKind.VariableDeclaration: + return true; + + // NOTE: Prior to TypeScript 3.7, in the emitted .d.ts files, the compiler would merge a GetAccessor/SetAccessor + // pair into a single PropertyDeclaration. + + // NOTE: In contexts where a source file is treated as a module, we do create "nominal analysis" + // AstSymbol objects corresponding to a ts.SyntaxKind.SourceFile node. However, a source file + // is NOT considered a nesting structure, and it does NOT act as a root for the declarations + // appearing in the file. This is because the *.d.ts generator is in the business of rolling up + // source files, and thus wants to ignore them in general. + default: + return false; + } + } +} diff --git a/packages/api-extractor/src/analyzer/AstEntity.ts b/packages/api-extractor/src/analyzer/AstEntity.ts new file mode 100644 index 000000000..ee08c1298 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstEntity.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * `AstEntity` is the abstract base class for analyzer objects that can become a `CollectorEntity`. + * + * @remarks + * + * The subclasses are: + * ``` + * - AstEntity + * - AstSymbol + * - AstSyntheticEntity + * - AstImport + * - AstNamespaceImport + * ``` + */ +export abstract class AstEntity { + /** + * The original name of the symbol, as exported from the module (i.e. source file) + * containing the original TypeScript definition. Constructs such as + * `import { X as Y } from` may introduce other names that differ from the local name. + * + * @remarks + * For the most part, `localName` corresponds to `followedSymbol.name`, but there + * are some edge cases. For example, the ts.Symbol.name for `export default class X { }` + * is actually `"default"`, not `"X"`. + */ + public abstract readonly localName: string; +} + +/** + * `AstSyntheticEntity` is the abstract base class for analyzer objects whose emitted declarations + * are not text transformations performed by the `Span` helper. + * + * @remarks + * Most of API Extractor's output is produced by using the using the `Span` utility to regurgitate strings from + * the input .d.ts files. If we need to rename an identifier, the `Span` visitor can pick out an interesting + * node and rewrite its string, but otherwise the transformation operates on dumb text and not compiler concepts. + * (Historically we did this because the compiler's emitter was an internal API, but it still has some advantages, + * for example preserving syntaxes generated by an older compiler to avoid incompatibilities.) + * + * This strategy does not work for cases where the output looks very different from the input. Today these + * cases are always kinds of `import` statements, but that may change in the future. + */ +export abstract class AstSyntheticEntity extends AstEntity {} diff --git a/packages/api-extractor/src/analyzer/AstImport.ts b/packages/api-extractor/src/analyzer/AstImport.ts new file mode 100644 index 000000000..4e50690e8 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstImport.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import { AstSyntheticEntity } from './AstEntity.js'; +import type { AstSymbol } from './AstSymbol.js'; + +/** + * Indicates the import kind for an `AstImport`. + */ +export enum AstImportKind { + /** + * An import statement such as `import X from "y";`. + */ + DefaultImport, + + /** + * An import statement such as `import { X } from "y";`. + */ + NamedImport, + + /** + * An import statement such as `import * as x from "y";`. + */ + StarImport, + + /** + * An import statement such as `import x = require("y");`. + */ + EqualsImport, + + /** + * An import statement such as `interface foo { foo: import("bar").a.b.c }`. + */ + ImportType, +} + +/** + * Constructor parameters for AstImport + * + * @privateRemarks + * Our naming convention is to use I____Parameters for constructor options and + * I____Options for general function options. However the word "parameters" is + * confusingly similar to the terminology for function parameters modeled by API Extractor, + * so we use I____Options for both cases in this code base. + */ +export interface IAstImportOptions { + readonly exportName: string; + readonly importKind: AstImportKind; + readonly isTypeOnly: boolean; + readonly modulePath: string; +} + +/** + * For a symbol that was imported from an external package, this tracks the import + * statement that was used to reach it. + */ +export class AstImport extends AstSyntheticEntity { + public readonly importKind: AstImportKind; + + /** + * The name of the external package (and possibly module path) that this definition + * was imported from. + * + * Example: "\@rushstack/node-core-library/lib/FileSystem" + */ + public readonly modulePath: string; + + /** + * The name of the symbol being imported. + * + * @remarks + * + * The name depends on the type of import: + * + * ```ts + * // For AstImportKind.DefaultImport style, exportName would be "X" in this example: + * import X from "y"; + * + * // For AstImportKind.NamedImport style, exportName would be "X" in this example: + * import { X } from "y"; + * + * // For AstImportKind.StarImport style, exportName would be "x" in this example: + * import * as x from "y"; + * + * // For AstImportKind.EqualsImport style, exportName would be "x" in this example: + * import x = require("y"); + * + * // For AstImportKind.ImportType style, exportName would be "a.b.c" in this example: + * interface foo { foo: import('bar').a.b.c }; + * ``` + */ + public readonly exportName: string; + + /** + * Whether it is a type-only import, for example: + * + * ```ts + * import type { X } from "y"; + * ``` + * + * This is set to true ONLY if the type-only form is used in *every* reference to this AstImport. + */ + public isTypeOnlyEverywhere: boolean; + + /** + * If this import statement refers to an API from an external package that is tracked by API Extractor + * (according to `PackageMetadataManager.isAedocSupportedFor()`), then this property will return the + * corresponding AstSymbol. Otherwise, it is undefined. + */ + public astSymbol: AstSymbol | undefined; + + /** + * If modulePath and exportName are defined, then this is a dictionary key + * that combines them with a colon (":"). + * + * Example: "\@rushstack/node-core-library/lib/FileSystem:FileSystem" + */ + public readonly key: string; + + public constructor(options: IAstImportOptions) { + super(); + + this.importKind = options.importKind; + this.modulePath = options.modulePath; + this.exportName = options.exportName; + + // We start with this assumption, but it may get changed later if non-type-only import is encountered. + this.isTypeOnlyEverywhere = options.isTypeOnly; + + this.key = AstImport.getKey(options); + } + + /** + * {@inheritdoc} + */ + public get localName(): string { + // abstract + return this.exportName; + } + + /** + * Calculates the lookup key used with `AstImport.key` + */ + public static getKey(options: IAstImportOptions): string { + switch (options.importKind) { + case AstImportKind.DefaultImport: + return `${options.modulePath}:${options.exportName}`; + case AstImportKind.NamedImport: + return `${options.modulePath}:${options.exportName}`; + case AstImportKind.StarImport: + return `${options.modulePath}:*`; + case AstImportKind.EqualsImport: + return `${options.modulePath}:=`; + case AstImportKind.ImportType: { + const subKey: string = options.exportName + ? options.exportName.includes('.') // Equivalent to a named export + ? options.exportName.split('.')[0]! + : options.exportName + : '*'; + return `${options.modulePath}:${subKey}`; + } + + default: + throw new InternalError('Unknown AstImportKind'); + } + } +} diff --git a/packages/api-extractor/src/analyzer/AstModule.ts b/packages/api-extractor/src/analyzer/AstModule.ts new file mode 100644 index 000000000..10b77f12d --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstModule.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as ts from 'typescript'; +import type { AstEntity } from './AstEntity.js'; +import type { AstSymbol } from './AstSymbol.js'; + +/** + * Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo} + */ +export class AstModuleExportInfo { + public readonly exportedLocalEntities: Map = new Map(); + + public readonly starExportedExternalModules: Set = new Set(); +} + +/** + * Constructor parameters for AstModule + * + * @privateRemarks + * Our naming convention is to use I____Parameters for constructor options and + * I____Options for general function options. However the word "parameters" is + * confusingly similar to the terminology for function parameters modeled by API Extractor, + * so we use I____Options for both cases in this code base. + */ +export interface IAstModuleOptions { + externalModulePath: string | undefined; + moduleSymbol: ts.Symbol; + sourceFile: ts.SourceFile; +} + +/** + * An internal data structure that represents a source file that is analyzed by AstSymbolTable. + */ +export class AstModule { + /** + * The source file that declares this TypeScript module. In most cases, the source file's + * top-level exports constitute the module. + */ + public readonly sourceFile: ts.SourceFile; + + /** + * The symbol for the module. Typically this corresponds to ts.SourceFile itself, however + * in some cases the ts.SourceFile may contain multiple modules declared using the `module` keyword. + */ + public readonly moduleSymbol: ts.Symbol; + + /** + * Example: "\@rushstack/node-core-library/lib/FileSystem" + * but never: "./FileSystem" + */ + public readonly externalModulePath: string | undefined; + + /** + * A list of other `AstModule` objects that appear in `export * from "___";` statements. + */ + public readonly starExportedModules: Set; + + /** + * A partial map of entities exported by this module. The key is the exported name. + */ + public readonly cachedExportedEntities: Map; // exportName --> entity + + /** + * Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`. + */ + public astModuleExportInfo: AstModuleExportInfo | undefined; + + public constructor(options: IAstModuleOptions) { + this.sourceFile = options.sourceFile; + this.moduleSymbol = options.moduleSymbol; + + this.externalModulePath = options.externalModulePath; + + this.starExportedModules = new Set(); + + this.cachedExportedEntities = new Map(); + + this.astModuleExportInfo = undefined; + } + + /** + * If false, then this source file is part of the working package being processed by the `Collector`. + */ + public get isExternal(): boolean { + return this.externalModulePath !== undefined; + } +} diff --git a/packages/api-extractor/src/analyzer/AstNamespaceImport.ts b/packages/api-extractor/src/analyzer/AstNamespaceImport.ts new file mode 100644 index 000000000..3741a5308 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstNamespaceImport.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +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'; + +export interface IAstNamespaceImportOptions { + readonly astModule: AstModule; + readonly declaration: ts.Declaration; + readonly namespaceName: string; + readonly symbol: ts.Symbol; +} + +/** + * `AstNamespaceImport` represents a namespace that is created implicitly by a statement + * such as `import * as example from "./file";` + * + * @remarks + * + * A typical input looks like this: + * ```ts + * // Suppose that example.ts exports two functions f1() and f2(). + * import * as example from "./file"; + * export { example }; + * ``` + * + * API Extractor's .d.ts rollup will transform it into an explicit namespace, like this: + * ```ts + * declare f1(): void; + * declare f2(): void; + * + * 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 `declare namespace example` is a synthetic construct represented by `AstNamespaceImport`. + */ +export class AstNamespaceImport extends AstSyntheticEntity { + /** + * Returns true if the AstSymbolTable.analyze() was called for this object. + * See that function for details. + */ + public analyzed: boolean = false; + + /** + * For example, if the original statement was `import * as example from "./file";` + * then `astModule` refers to the `./file.d.ts` file. + */ + public readonly astModule: AstModule; + + /** + * For example, if the original statement was `import * as example from "./file";` + * then `namespaceName` would be `example`. + */ + public readonly namespaceName: string; + + /** + * The original `ts.SyntaxKind.NamespaceImport` which can be used as a location for error messages. + */ + public readonly declaration: ts.Declaration; + + /** + * The original `ts.SymbolFlags.Namespace` symbol. + */ + public readonly symbol: ts.Symbol; + + public constructor(options: IAstNamespaceImportOptions) { + super(); + this.astModule = options.astModule; + this.namespaceName = options.namespaceName; + this.declaration = options.declaration; + this.symbol = options.symbol; + } + + /** + * {@inheritdoc} + */ + public get localName(): string { + // abstract + return this.namespaceName; + } + + public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo { + const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule); + return astModuleExportInfo; + } +} diff --git a/packages/api-extractor/src/analyzer/AstReferenceResolver.ts b/packages/api-extractor/src/analyzer/AstReferenceResolver.ts new file mode 100644 index 000000000..d0e3b1c8e --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstReferenceResolver.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +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 { AstDeclaration } from './AstDeclaration.js'; +import type { AstEntity } from './AstEntity.js'; +import type { AstModule } from './AstModule.js'; +import { AstSymbol } from './AstSymbol.js'; +import type { AstSymbolTable } from './AstSymbolTable.js'; + +/** + * Used by `AstReferenceResolver` to report a failed resolution. + * + * @privateRemarks + * This class is similar to an `Error` object, but the intent of `ResolverFailure` is to describe + * why a reference could not be resolved. This information could be used to throw an actual `Error` object, + * but normally it is handed off to the `MessageRouter` instead. + */ +export class ResolverFailure { + /** + * Details about why the failure occurred. + */ + public readonly reason: string; + + public constructor(reason: string) { + this.reason = reason; + } +} + +/** + * This resolves a TSDoc declaration reference by walking the `AstSymbolTable` compiler state. + * + * @remarks + * + * This class is analogous to `ModelReferenceResolver` from the `@microsoft/api-extractor-model` project, + * which resolves declaration references by walking the hierarchy loaded from an .api.json file. + */ +export class AstReferenceResolver { + private readonly _collector: Collector; + + private readonly _astSymbolTable: AstSymbolTable; + + private readonly _workingPackage: WorkingPackage; + + public constructor(collector: Collector) { + this._collector = collector; + this._astSymbolTable = collector.astSymbolTable; + this._workingPackage = collector.workingPackage; + } + + public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure { + // Is it referring to the working package? + if ( + declarationReference.packageName !== undefined && + declarationReference.packageName !== this._workingPackage.name + ) { + return new ResolverFailure('External package references are not supported'); + } + + // Is it a path-based import? + if (declarationReference.importPath) { + return new ResolverFailure('Import paths are not supported'); + } + + const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage( + this._workingPackage.entryPointSourceFile, + ); + + if (declarationReference.memberReferences.length === 0) { + return new ResolverFailure('Package references are not supported'); + } + + const rootMemberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[0]!; + + const exportName: ResolverFailure | string = this._getMemberReferenceIdentifier(rootMemberReference); + if (exportName instanceof ResolverFailure) { + return exportName; + } + + const rootAstEntity: AstEntity | undefined = this._astSymbolTable.tryGetExportOfAstModule(exportName, astModule); + + if (rootAstEntity === undefined) { + return new ResolverFailure(`The package "${this._workingPackage.name}" does not have an export "${exportName}"`); + } + + if (!(rootAstEntity instanceof AstSymbol)) { + return new ResolverFailure('This type of declaration is not supported yet by the resolver'); + } + + let currentDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration( + rootAstEntity.astDeclarations, + rootMemberReference, + rootAstEntity.localName, + ); + + if (currentDeclaration instanceof ResolverFailure) { + return currentDeclaration; + } + + for (let index = 1; index < declarationReference.memberReferences.length; ++index) { + const memberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[index]!; + + const memberName: ResolverFailure | string = this._getMemberReferenceIdentifier(memberReference); + if (memberName instanceof ResolverFailure) { + return memberName; + } + + const matchingChildren: readonly AstDeclaration[] = currentDeclaration.findChildrenWithName(memberName); + if (matchingChildren.length === 0) { + return new ResolverFailure(`No member was found with name "${memberName}"`); + } + + const selectedDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration( + matchingChildren, + memberReference, + memberName, + ); + + if (selectedDeclaration instanceof ResolverFailure) { + return selectedDeclaration; + } + + currentDeclaration = selectedDeclaration; + } + + return currentDeclaration; + } + + private _getMemberReferenceIdentifier(memberReference: tsdoc.DocMemberReference): ResolverFailure | string { + if (memberReference.memberSymbol !== undefined) { + return new ResolverFailure('ECMAScript symbol selectors are not supported'); + } + + if (memberReference.memberIdentifier === undefined) { + return new ResolverFailure('The member identifier is missing in the root member reference'); + } + + return memberReference.memberIdentifier.identifier; + } + + private _selectDeclaration( + astDeclarations: readonly AstDeclaration[], + memberReference: tsdoc.DocMemberReference, + astSymbolName: string, + ): AstDeclaration | ResolverFailure { + const memberSelector: tsdoc.DocMemberSelector | undefined = memberReference.selector; + + if (memberSelector === undefined) { + if (astDeclarations.length === 1) { + return astDeclarations[0]!; + } else { + // If we found multiple matches, but the extra ones are all ancillary declarations, + // then return the main declaration. + const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(astDeclarations); + if (nonAncillaryMatch) { + return nonAncillaryMatch; + } + + return new ResolverFailure( + `The reference is ambiguous because "${astSymbolName}"` + + ` has more than one declaration; you need to add a TSDoc member reference selector`, + ); + } + } + + switch (memberSelector.selectorKind) { + case tsdoc.SelectorKind.System: + return this._selectUsingSystemSelector(astDeclarations, memberSelector, astSymbolName); + case tsdoc.SelectorKind.Index: + return this._selectUsingIndexSelector(astDeclarations, memberSelector, astSymbolName); + default: + return new ResolverFailure(`The selector "${memberSelector.selector}" is not a supported selector type`); + } + } + + private _selectUsingSystemSelector( + astDeclarations: readonly AstDeclaration[], + memberSelector: tsdoc.DocMemberSelector, + astSymbolName: string, + ): AstDeclaration | ResolverFailure { + const selectorName: string = memberSelector.selector; + + let selectorSyntaxKind: ts.SyntaxKind; + + switch (selectorName) { + case 'class': + selectorSyntaxKind = ts.SyntaxKind.ClassDeclaration; + break; + case 'enum': + selectorSyntaxKind = ts.SyntaxKind.EnumDeclaration; + break; + case 'function': + selectorSyntaxKind = ts.SyntaxKind.FunctionDeclaration; + break; + case 'interface': + selectorSyntaxKind = ts.SyntaxKind.InterfaceDeclaration; + break; + case 'namespace': + selectorSyntaxKind = ts.SyntaxKind.ModuleDeclaration; + break; + case 'type': + selectorSyntaxKind = ts.SyntaxKind.TypeAliasDeclaration; + break; + case 'variable': + selectorSyntaxKind = ts.SyntaxKind.VariableDeclaration; + break; + default: + return new ResolverFailure(`Unsupported system selector "${selectorName}"`); + } + + const matches: AstDeclaration[] = astDeclarations.filter((x) => x.declaration.kind === selectorSyntaxKind); + if (matches.length === 0) { + return new ResolverFailure( + `A declaration for "${astSymbolName}" was not found that matches the TSDoc selector "${selectorName}"`, + ); + } + + if (matches.length > 1) { + // If we found multiple matches, but the extra ones are all ancillary declarations, + // then return the main declaration. + const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches); + if (nonAncillaryMatch) { + return nonAncillaryMatch; + } + + return new ResolverFailure( + `More than one declaration "${astSymbolName}" matches the TSDoc selector "${selectorName}"`, + ); + } + + return matches[0]!; + } + + private _selectUsingIndexSelector( + astDeclarations: readonly AstDeclaration[], + memberSelector: tsdoc.DocMemberSelector, + astSymbolName: string, + ): AstDeclaration | ResolverFailure { + const selectorOverloadIndex: number = Number.parseInt(memberSelector.selector, 10); + + const matches: AstDeclaration[] = []; + for (const astDeclaration of astDeclarations) { + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + if (overloadIndex === selectorOverloadIndex) { + matches.push(astDeclaration); + } + } + + if (matches.length === 0) { + return new ResolverFailure( + `An overload for "${astSymbolName}" was not found that matches the` + + ` TSDoc selector ":${selectorOverloadIndex}"`, + ); + } + + if (matches.length > 1) { + // If we found multiple matches, but the extra ones are all ancillary declarations, + // then return the main declaration. + const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches); + if (nonAncillaryMatch) { + return nonAncillaryMatch; + } + + return new ResolverFailure( + `More than one declaration for "${astSymbolName}" matches the TSDoc selector ":${selectorOverloadIndex}"`, + ); + } + + return matches[0]!; + } + + /** + * This resolves an ambiguous match in the case where the extra matches are all ancillary declarations, + * except for one match that is the main declaration. + */ + private _tryDisambiguateAncillaryMatches(matches: readonly AstDeclaration[]): AstDeclaration | undefined { + let result: AstDeclaration | undefined; + + for (const match of matches) { + const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(match); + if (!declarationMetadata.isAncillary) { + if (result) { + return undefined; // more than one match + } + + result = match; + } + } + + return result; + } +} diff --git a/packages/api-extractor/src/analyzer/AstSymbol.ts b/packages/api-extractor/src/analyzer/AstSymbol.ts new file mode 100644 index 000000000..40572dbd1 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstSymbol.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import type * as ts from 'typescript'; +import type { AstDeclaration } from './AstDeclaration.js'; +import { AstEntity } from './AstEntity.js'; + +/** + * Constructor options for AstSymbol + */ +export interface IAstSymbolOptions { + readonly followedSymbol: ts.Symbol; + readonly isExternal: boolean; + readonly localName: string; + readonly nominalAnalysis: boolean; + readonly parentAstSymbol: AstSymbol | undefined; + readonly rootAstSymbol: AstSymbol | undefined; +} + +/** + * The AstDeclaration and AstSymbol classes are API Extractor's equivalent of the compiler's + * ts.Declaration and ts.Symbol objects. They are created by the `AstSymbolTable` class. + * + * @remarks + * The AstSymbol represents the ts.Symbol information for an AstDeclaration. For example, + * if a method has 3 overloads, each overloaded signature will have its own AstDeclaration, + * but they will all share a common AstSymbol. + * + * For nested definitions, the AstSymbol has a unique parent (i.e. AstSymbol.rootAstSymbol), + * but the parent/children for each AstDeclaration may be different. Consider this example: + * + * ```ts + * export namespace N { + * export function f(): void { } + * } + * + * export interface N { + * g(): void; + * } + * ``` + * + * Note how the parent/child relationships are different for the symbol tree versus + * the declaration tree, and the declaration tree has two roots: + * + * ``` + * AstSymbol tree: AstDeclaration tree: + * - N - N (namespace) + * - f - f + * - g - N (interface) + * - g + * ``` + */ +export class AstSymbol extends AstEntity { + /** + * {@inheritdoc} + */ + public readonly localName: string; // abstract + + /** + * If true, then the `followedSymbol` (i.e. original declaration) of this symbol + * is not part of the working package. The working package may still export this symbol, + * but if so it should be emitted as an alias such as `export { X } from "package1";`. + */ + public readonly isExternal: boolean; + + /** + * The compiler symbol where this type was defined, after following any aliases. + * + * @remarks + * This is a normal form that can be reached from any symbol alias by calling + * `TypeScriptHelpers.followAliases()`. It can be compared to determine whether two + * symbols refer to the same underlying type. + */ + public readonly followedSymbol: ts.Symbol; + + /** + * If true, then this AstSymbol represents a foreign object whose structure will be + * ignored. The AstDeclaration objects will not have any parent or children, and its references + * will not be analyzed. + * + * Nominal symbols are tracked e.g. when they are reexported by the working package. + */ + public readonly nominalAnalysis: boolean; + + /** + * Returns the symbol of the parent of this AstSymbol, or undefined if there is no parent. + * + * @remarks + * If a symbol has multiple declarations, we assume (as an axiom) that their parent + * declarations will belong to the same symbol. This means that the "parent" of a + * symbol is a well-defined concept. However, the "children" of a symbol are not very + * meaningful, because different declarations may have different nested members, + * so we usually need to traverse declarations to find children. + */ + public readonly parentAstSymbol: AstSymbol | undefined; + + /** + * Returns the symbol of the root of the AstDeclaration hierarchy. + * + * @remarks + * NOTE: If this AstSymbol is the root, then rootAstSymbol will point to itself. + */ + public readonly rootAstSymbol: AstSymbol; + + /** + * Additional information that is calculated later by the `Collector`. The actual type is `SymbolMetadata`, + * but we declare it as `unknown` because consumers must obtain this object by calling + * `Collector.fetchSymbolMetadata()`. + */ + public symbolMetadata: unknown; + + private readonly _astDeclarations: AstDeclaration[]; + + // This flag is unused if this is not the root symbol. + // Being "analyzed" is a property of the root symbol. + private _analyzed: boolean = false; + + public constructor(options: IAstSymbolOptions) { + super(); + + this.followedSymbol = options.followedSymbol; + this.localName = options.localName; + this.isExternal = options.isExternal; + this.nominalAnalysis = options.nominalAnalysis; + this.parentAstSymbol = options.parentAstSymbol; + this.rootAstSymbol = options.rootAstSymbol ?? this; + this._astDeclarations = []; + } + + /** + * The one or more declarations for this symbol. + * + * @remarks + * For example, if this symbol is a method, then the declarations might be + * various method overloads. If this symbol is a namespace, then the declarations + * might be separate namespace blocks with the same name that get combined via + * declaration merging. + */ + public get astDeclarations(): readonly AstDeclaration[] { + return this._astDeclarations; + } + + /** + * Returns true if the AstSymbolTable.analyze() was called for this object. + * See that function for details. + * + * @remarks + * AstSymbolTable.analyze() is always performed on the root AstSymbol. This function + * returns true if-and-only-if the root symbol was analyzed. + */ + public get analyzed(): boolean { + return this.rootAstSymbol._analyzed; + } + + /** + * This is an internal callback used when the AstSymbolTable attaches a new + * AstDeclaration to this object. + * + * @internal + */ + public _notifyDeclarationAttach(astDeclaration: AstDeclaration): void { + if (this.analyzed) { + throw new InternalError('_notifyDeclarationAttach() called after analysis is already complete'); + } + + this._astDeclarations.push(astDeclaration); + } + + /** + * This is an internal callback used when the AstSymbolTable.analyze() + * has processed this object. + * + * @internal + */ + public _notifyAnalyzed(): void { + if (this.parentAstSymbol) { + throw new InternalError('_notifyAnalyzed() called for an AstSymbol which is not the root'); + } + + this._analyzed = true; + } + + /** + * Helper that calls AstDeclaration.forEachDeclarationRecursive() for each AstDeclaration. + */ + public forEachDeclarationRecursive(action: (astDeclaration: AstDeclaration) => void): void { + for (const astDeclaration of this.astDeclarations) { + astDeclaration.forEachDeclarationRecursive(action); + } + } +} diff --git a/packages/api-extractor/src/analyzer/AstSymbolTable.ts b/packages/api-extractor/src/analyzer/AstSymbolTable.ts new file mode 100644 index 000000000..6acc02003 --- /dev/null +++ b/packages/api-extractor/src/analyzer/AstSymbolTable.ts @@ -0,0 +1,709 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// for ts.SymbolFlags + +import { type PackageJsonLookup, InternalError } from '@rushstack/node-core-library'; +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 { AstNamespaceImport } from './AstNamespaceImport.js'; +import { AstSymbol } from './AstSymbol.js'; +import { ExportAnalyzer } from './ExportAnalyzer.js'; +import { PackageMetadataManager } from './PackageMetadataManager.js'; +import { SourceFileLocationFormatter } from './SourceFileLocationFormatter.js'; +import { SyntaxHelpers } from './SyntaxHelpers.js'; +import { TypeScriptHelpers } from './TypeScriptHelpers.js'; +import { TypeScriptInternals, type IGlobalVariableAnalyzer } from './TypeScriptInternals.js'; + +/** + * Options for `AstSymbolTable._fetchAstSymbol()` + */ +export interface IFetchAstSymbolOptions { + /** + * True while populating the `AstSymbolTable`; false if we're doing a passive lookup + * without adding anything new to the table + */ + addIfMissing: boolean; + /** + * The symbol after any symbol aliases have been followed using TypeScriptHelpers.followAliases() + */ + followedSymbol: ts.Symbol; + + /** + * If true, symbols with AstSymbol.nominalAnalysis=true will be returned. + * Otherwise `undefined` will be returned for such symbols. + */ + includeNominalAnalysis: boolean; + + /** + * True if followedSymbol is not part of the working package + */ + isExternal: boolean; + + /** + * A hint to help `_fetchAstSymbol()` determine the `AstSymbol.localName`. + */ + localName?: string; +} + +/** + * AstSymbolTable is the workhorse that builds AstSymbol and AstDeclaration objects. + * It maintains a cache of already constructed objects. AstSymbolTable constructs + * AstModule objects, but otherwise the state that it maintains is agnostic of + * any particular entry point. (For example, it does not track whether a given AstSymbol + * is "exported" or not.) + * + * Internally, AstSymbolTable relies on ExportAnalyzer to crawl import statements and determine where symbols + * are declared (i.e. the AstImport information needed to import them). + */ +export class AstSymbolTable { + private readonly _program: ts.Program; + + private readonly _typeChecker: ts.TypeChecker; + + private readonly _messageRouter: MessageRouter; + + private readonly _globalVariableAnalyzer: IGlobalVariableAnalyzer; + + private readonly _packageMetadataManager: PackageMetadataManager; + + private readonly _exportAnalyzer: ExportAnalyzer; + + private readonly _alreadyWarnedGlobalNames: Set; + + /** + * A mapping from ts.Symbol --\> AstSymbol + * NOTE: The AstSymbol.followedSymbol will always be a lookup key, but additional keys + * are possible. + * + * After following type aliases, we use this map to look up the corresponding AstSymbol. + */ + private readonly _astSymbolsBySymbol: Map = new Map(); + + /** + * A mapping from ts.Declaration --\> AstDeclaration + */ + private readonly _astDeclarationsByDeclaration: Map = new Map(); + + // Note that this is a mapping from specific AST nodes that we analyzed, based on the underlying symbol + // for that node. + private readonly _entitiesByNode: Map = new Map< + ts.Identifier, + AstEntity | undefined + >(); + + public constructor( + program: ts.Program, + typeChecker: ts.TypeChecker, + packageJsonLookup: PackageJsonLookup, + bundledPackageNames: ReadonlySet, + messageRouter: MessageRouter, + ) { + this._program = program; + this._typeChecker = typeChecker; + this._messageRouter = messageRouter; + this._globalVariableAnalyzer = TypeScriptInternals.getGlobalVariableAnalyzer(program); + this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, messageRouter); + + this._exportAnalyzer = new ExportAnalyzer(this._program, this._typeChecker, bundledPackageNames, { + analyze: this.analyze.bind(this), + fetchAstSymbol: this._fetchAstSymbol.bind(this), + }); + + this._alreadyWarnedGlobalNames = new Set(); + } + + /** + * Used to analyze an entry point that belongs to the working package. + */ + public fetchAstModuleFromWorkingPackage(sourceFile: ts.SourceFile): AstModule { + return this._exportAnalyzer.fetchAstModuleFromSourceFile(sourceFile, undefined, false); + } + + /** + * This crawls the specified entry point and collects the full set of exported AstSymbols. + */ + public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo { + return this._exportAnalyzer.fetchAstModuleExportInfo(astModule); + } + + /** + * Attempts to retrieve an export by name from the specified `AstModule`. + * Returns undefined if no match was found. + */ + public tryGetExportOfAstModule(exportName: string, astModule: AstModule): AstEntity | undefined { + return this._exportAnalyzer.tryGetExportOfAstModule(exportName, astModule); + } + + /** + * Ensures that AstSymbol.analyzed is true for the provided symbol. The operation + * starts from the root symbol and then fills out all children of all declarations, and + * also calculates AstDeclaration.referencedAstSymbols for all declarations. + * If the symbol is not imported, any non-imported references are also analyzed. + * + * @remarks + * This is an expensive operation, so we only perform it for top-level exports of an + * the AstModule. For example, if some code references a nested class inside + * a namespace from another library, we do not analyze any of that class's siblings + * or members. (We do always construct its parents however, since AstDefinition.parent + * is immutable, and needed e.g. to calculate release tag inheritance.) + */ + public analyze(astEntity: AstEntity): void { + if (astEntity instanceof AstSymbol) { + this._analyzeAstSymbol(astEntity); + return; + } + + if (astEntity instanceof AstNamespaceImport) { + this._analyzeAstNamespaceImport(astEntity); + } + } + + /** + * For a given astDeclaration, this efficiently finds the child corresponding to the + * specified ts.Node. It is assumed that AstDeclaration.isSupportedSyntaxKind() would return true for + * that node type, and that the node is an immediate child of the provided AstDeclaration. + */ + // NOTE: This could be a method of AstSymbol if it had a backpointer to its AstSymbolTable. + public getChildAstDeclarationByNode(node: ts.Node, parentAstDeclaration: AstDeclaration): AstDeclaration { + if (!parentAstDeclaration.astSymbol.analyzed) { + throw new Error('getChildDeclarationByNode() cannot be used for an AstSymbol that was not analyzed'); + } + + const childAstDeclaration: AstDeclaration | undefined = this._astDeclarationsByDeclaration.get(node); + if (!childAstDeclaration) { + throw new Error('Child declaration not found for the specified node'); + } + + if (childAstDeclaration.parent !== parentAstDeclaration) { + throw new InternalError('The found child is not attached to the parent AstDeclaration'); + } + + return childAstDeclaration; + } + + /** + * For a given ts.Identifier that is part of an AstSymbol that we analyzed, return the AstEntity that + * it refers to. Returns undefined if it doesn't refer to anything interesting. + * + * @remarks + * Throws an Error if the ts.Identifier is not part of node tree that was analyzed. + */ + public tryGetEntityForNode(identifier: ts.Identifier | ts.ImportTypeNode): AstEntity | undefined { + if (!this._entitiesByNode.has(identifier)) { + throw new InternalError('tryGetEntityForIdentifier() called for an identifier that was not analyzed'); + } + + return this._entitiesByNode.get(identifier); + } + + /** + * Builds an AstSymbol.localName for a given ts.Symbol. In the current implementation, the localName is + * a TypeScript-like expression that may be a string literal or ECMAScript symbol expression. + * + * ```ts + * class X { + * // localName="identifier" + * public identifier: number = 1; + * // localName="\"identifier\"" + * public "quoted string!": number = 2; + * // localName="[MyNamespace.MySymbol]" + * public [MyNamespace.MySymbol]: number = 3; + * } + * ``` + */ + public static getLocalNameForSymbol(symbol: ts.Symbol): string { + // TypeScript binds well-known ECMAScript symbols like "[Symbol.iterator]" as "__@iterator". + // Decode it back into "[Symbol.iterator]". + const wellKnownSymbolName: string | undefined = TypeScriptHelpers.tryDecodeWellKnownSymbolName(symbol.escapedName); + if (wellKnownSymbolName) { + return wellKnownSymbolName; + } + + const isUniqueSymbol: boolean = TypeScriptHelpers.isUniqueSymbolName(symbol.escapedName); + + // We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name. + let unquotedName: string = symbol.name; + + for (const declaration of symbol.declarations ?? []) { + // Handle cases such as "export default class X { }" where the symbol name is "default" + // but the local name is "X". + const localSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetLocalSymbol(declaration); + if (localSymbol) { + unquotedName = localSymbol.name; + } + + // If it is a non-well-known symbol, then return the late-bound name. For example, "X.Y.z" in this example: + // + // namespace X { + // export namespace Y { + // export const z: unique symbol = Symbol("z"); + // } + // } + // + // class C { + // public [X.Y.z](): void { } + // } + // + if (isUniqueSymbol) { + const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration); + if (declarationName && ts.isComputedPropertyName(declarationName)) { + const lateBoundName: string | undefined = TypeScriptHelpers.tryGetLateBoundName(declarationName); + if (lateBoundName) { + // Here the string may contain an expression such as "[X.Y.z]". Names starting with "[" are always + // expressions. If a string literal contains those characters, the code below will JSON.stringify() it + // to avoid a collision. + return lateBoundName; + } + } + } + } + + // Otherwise that name may come from a quoted string or pseudonym like `__constructor`. + // If the string is not a safe identifier, then we must add quotes. + // Note that if it was quoted but did not need to be quoted, here we will remove the quotes. + if (!SyntaxHelpers.isSafeUnquotedMemberIdentifier(unquotedName)) { + // For API Extractor's purposes, a canonical form is more appropriate than trying to reflect whatever + // appeared in the source code. The code is not even guaranteed to be consistent, for example: + // + // class X { + // public "f1"(x: string): void; + // public f1(x: boolean): void; + // public 'f1'(x: string | boolean): void { } + // } + return JSON.stringify(unquotedName); + } + + return unquotedName; + } + + private _analyzeAstNamespaceImport(astNamespaceImport: AstNamespaceImport): void { + if (astNamespaceImport.analyzed) { + return; + } + + // mark before actual analyzing, to handle module cyclic reexport + astNamespaceImport.analyzed = true; + + const exportedLocalEntities: Map = this.fetchAstModuleExportInfo( + astNamespaceImport.astModule, + ).exportedLocalEntities; + + for (const exportedEntity of exportedLocalEntities.values()) { + this.analyze(exportedEntity); + } + } + + private _analyzeAstSymbol(astSymbol: AstSymbol): void { + if (astSymbol.analyzed) { + return; + } + + if (astSymbol.nominalAnalysis) { + // We don't analyze nominal symbols + astSymbol._notifyAnalyzed(); + return; + } + + // Start at the root of the tree + const rootAstSymbol: AstSymbol = astSymbol.rootAstSymbol; + + // Calculate the full child tree for each definition + for (const astDeclaration of rootAstSymbol.astDeclarations) { + this._analyzeChildTree(astDeclaration.declaration, astDeclaration); + } + + rootAstSymbol._notifyAnalyzed(); + + if (!astSymbol.isExternal) { + // If this symbol is non-external (i.e. it belongs to the working package), then we also analyze any + // referencedAstSymbols that are non-external. For example, this ensures that forgotten exports + // get analyzed. + rootAstSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + for (const referencedAstEntity of astDeclaration.referencedAstEntities) { + // Walk up to the root of the tree, looking for any imports along the way + if (referencedAstEntity instanceof AstSymbol && !referencedAstEntity.isExternal) { + this._analyzeAstSymbol(referencedAstEntity); + } + + if (referencedAstEntity instanceof AstNamespaceImport && !referencedAstEntity.astModule.isExternal) { + this._analyzeAstNamespaceImport(referencedAstEntity); + } + } + }); + } + } + + /** + * Used by analyze to recursively analyze the entire child tree. + */ + private _analyzeChildTree(node: ts.Node, governingAstDeclaration: AstDeclaration): void { + switch (node.kind) { + case ts.SyntaxKind.JSDocComment: // Skip JSDoc comments - TS considers @param tags TypeReference nodes + return; + + // Is this a reference to another AstSymbol? + case ts.SyntaxKind.TypeReference: // general type references + case ts.SyntaxKind.ExpressionWithTypeArguments: // special case for e.g. the "extends" keyword + case ts.SyntaxKind.ComputedPropertyName: // used for EcmaScript "symbols", e.g. "[toPrimitive]". + case ts.SyntaxKind.TypeQuery: // represents for "typeof X" as a type + { + // Sometimes the type reference will involve multiple identifiers, e.g. "a.b.C". + // In this case, we only need to worry about importing the first identifier, + // so do a depth-first search for it: + const identifierNode: ts.Identifier | undefined = TypeScriptHelpers.findFirstChildNode( + node, + ts.SyntaxKind.Identifier, + ); + + if (identifierNode) { + let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(identifierNode); + if (!referencedAstEntity) { + const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(identifierNode); + if (!symbol) { + throw new Error('Symbol not found for identifier: ' + identifierNode.getText()); + } + + // Normally we expect getSymbolAtLocation() to take us to a declaration within the same source + // file, or else to an explicit "import" statement within the same source file. But in certain + // situations (e.g. a global variable) the symbol will refer to a declaration in some other + // source file. We'll call that case a "displaced symbol". + // + // For more info, see this discussion: + // https://github.com/microsoft/rushstack/issues/1765#issuecomment-595559849 + let displacedSymbol = true; + for (const declaration of symbol.declarations ?? []) { + if (declaration.getSourceFile() === identifierNode.getSourceFile()) { + displacedSymbol = false; + break; + } + } + + if (displacedSymbol) { + if (this._globalVariableAnalyzer.hasGlobalName(identifierNode.text)) { + // If the displaced symbol is a global variable, then API Extractor simply ignores it. + // Ambient declarations typically describe the runtime environment (provided by an API consumer), + // so we don't bother analyzing them as an API contract. (There are probably some packages + // that include interesting global variables in their API, but API Extractor doesn't support + // that yet; it would be a feature request.) + + if (this._messageRouter.showDiagnostics && !this._alreadyWarnedGlobalNames.has(identifierNode.text)) { + this._alreadyWarnedGlobalNames.add(identifierNode.text); + this._messageRouter.logDiagnostic( + `Ignoring reference to global variable "${identifierNode.text}"` + + ` in ` + + SourceFileLocationFormatter.formatDeclaration(identifierNode), + ); + } + } else { + // If you encounter this, please report a bug with a repro. We're interested to know + // how it can occur. + throw new InternalError(`Unable to follow symbol for "${identifierNode.text}"`); + } + } else { + referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity( + symbol, + governingAstDeclaration.astSymbol.isExternal, + ); + + this._entitiesByNode.set(identifierNode, referencedAstEntity); + } + } + + if (referencedAstEntity) { + governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity); + } + } + } + + break; + + // Is this the identifier for the governingAstDeclaration? + case ts.SyntaxKind.Identifier: + { + const identifierNode: ts.Identifier = node as ts.Identifier; + if (!this._entitiesByNode.has(identifierNode)) { + const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(identifierNode); + + let referencedAstEntity: AstEntity | undefined; + + if (symbol === governingAstDeclaration.astSymbol.followedSymbol) { + referencedAstEntity = this._fetchEntityForNode(identifierNode, governingAstDeclaration); + } + + this._entitiesByNode.set(identifierNode, referencedAstEntity); + } + } + + break; + + case ts.SyntaxKind.ImportType: + { + const importTypeNode: ts.ImportTypeNode = node as ts.ImportTypeNode; + let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(importTypeNode); + + if (!this._entitiesByNode.has(importTypeNode)) { + referencedAstEntity = this._fetchEntityForNode(importTypeNode, governingAstDeclaration); + + if (!referencedAstEntity) { + // This should never happen + throw new Error('Failed to fetch entity for import() type node: ' + importTypeNode.getText()); + } + + this._entitiesByNode.set(importTypeNode, referencedAstEntity); + } + + if (referencedAstEntity) { + governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity); + } + } + + break; + + default: + break; + } + + // Is this node declaring a new AstSymbol? + const newGoverningAstDeclaration: AstDeclaration | undefined = this._fetchAstDeclaration( + node, + governingAstDeclaration.astSymbol.isExternal, + ); + + for (const childNode of node.getChildren()) { + this._analyzeChildTree(childNode, newGoverningAstDeclaration ?? governingAstDeclaration); + } + } + + private _fetchEntityForNode( + node: ts.Identifier | ts.ImportTypeNode, + governingAstDeclaration: AstDeclaration, + ): AstEntity | undefined { + let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(node); + if (!referencedAstEntity) { + if (node.kind === ts.SyntaxKind.ImportType) { + referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntityFromImportTypeNode( + node, + governingAstDeclaration.astSymbol.isExternal, + ); + } else { + const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(node); + if (!symbol) { + throw new Error('Symbol not found for identifier: ' + node.getText()); + } + + referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity( + symbol, + governingAstDeclaration.astSymbol.isExternal, + ); + } + + this._entitiesByNode.set(node, referencedAstEntity); + } + + return referencedAstEntity; + } + + private _fetchAstDeclaration(node: ts.Node, isExternal: boolean): AstDeclaration | undefined { + if (!AstDeclaration.isSupportedSyntaxKind(node.kind)) { + return undefined; + } + + const symbol: ts.Symbol | undefined = TypeScriptHelpers.getSymbolForDeclaration( + node as ts.Declaration, + this._typeChecker, + ); + if (!symbol) { + throw new InternalError('Unable to find symbol for node'); + } + + const astSymbol: AstSymbol | undefined = this._fetchAstSymbol({ + followedSymbol: symbol, + isExternal, + includeNominalAnalysis: true, + addIfMissing: true, + }); + + if (!astSymbol) { + return undefined; + } + + const astDeclaration: AstDeclaration | undefined = this._astDeclarationsByDeclaration.get(node); + + if (!astDeclaration) { + throw new InternalError('Unable to find constructed AstDeclaration'); + } + + return astDeclaration; + } + + private _fetchAstSymbol(options: IFetchAstSymbolOptions): AstSymbol | undefined { + const followedSymbol: ts.Symbol = options.followedSymbol; + + // Filter out symbols representing constructs that we don't care about + const arbitraryDeclaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(followedSymbol); + if (!arbitraryDeclaration) { + return undefined; + } + + if ( + followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient) && + !TypeScriptInternals.isLateBoundSymbol(followedSymbol) + ) { + return undefined; + } + + // API Extractor doesn't analyze ambient declarations at all + if ( + TypeScriptHelpers.isAmbient(followedSymbol, this._typeChecker) && // We make a special exemption for ambient declarations that appear in a source file containing + // an "export=" declaration that allows them to be imported as non-ambient. + !this._exportAnalyzer.isImportableAmbientSourceFile(arbitraryDeclaration.getSourceFile()) + ) { + return undefined; + } + + // Make sure followedSymbol isn't an alias for something else + if (TypeScriptHelpers.isFollowableAlias(followedSymbol, this._typeChecker)) { + // We expect the caller to have already followed any aliases + throw new InternalError('AstSymbolTable._fetchAstSymbol() cannot be called with a symbol alias'); + } + + let astSymbol: AstSymbol | undefined = this._astSymbolsBySymbol.get(followedSymbol); + + if (!astSymbol) { + // None of the above lookups worked, so create a new entry... + let nominalAnalysis = false; + + if (options.isExternal) { + // If the file is from an external package that does not support AEDoc, normally we ignore it completely. + // But in some cases (e.g. checking star exports of an external package) we need an AstSymbol to + // represent it, but we don't need to analyze its sibling/children. + const followedSymbolSourceFileName: string = arbitraryDeclaration.getSourceFile().fileName; + + if (!this._packageMetadataManager.isAedocSupportedFor(followedSymbolSourceFileName)) { + nominalAnalysis = true; + + if (!options.includeNominalAnalysis) { + return undefined; + } + } + } + + let parentAstSymbol: AstSymbol | undefined; + + if (!nominalAnalysis) { + for (const declaration of followedSymbol.declarations ?? []) { + if (!AstDeclaration.isSupportedSyntaxKind(declaration.kind)) { + throw new InternalError( + `The "${followedSymbol.name}" symbol has a` + + ` ts.SyntaxKind.${ts.SyntaxKind[declaration.kind]} declaration which is not (yet?)` + + ` supported by API Extractor`, + ); + } + } + + // We always fetch the entire chain of parents for each declaration. + // (Children/siblings are only analyzed on demand.) + + // Key assumptions behind this squirrely logic: + // + // IF a given symbol has two declarations D1 and D2; AND + // If D1 has a parent P1, then + // - D2 will also have a parent P2; AND + // - P1 and P2's symbol will be the same + // - but P1 and P2 may be different (e.g. merged namespaces containing merged interfaces) + + // Is there a parent AstSymbol? First we check to see if there is a parent declaration: + if (arbitraryDeclaration) { + const arbitraryParentDeclaration: ts.Node | undefined = + this._tryFindFirstAstDeclarationParent(arbitraryDeclaration); + + if (arbitraryParentDeclaration) { + const parentSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration( + arbitraryParentDeclaration as ts.Declaration, + this._typeChecker, + ); + + parentAstSymbol = this._fetchAstSymbol({ + followedSymbol: parentSymbol, + isExternal: options.isExternal, + includeNominalAnalysis: false, + addIfMissing: true, + }); + if (!parentAstSymbol) { + throw new InternalError('Unable to construct a parent AstSymbol for ' + followedSymbol.name); + } + } + } + } + + const localName: string | undefined = options.localName ?? AstSymbolTable.getLocalNameForSymbol(followedSymbol); + + astSymbol = new AstSymbol({ + followedSymbol, + localName, + isExternal: options.isExternal, + nominalAnalysis, + parentAstSymbol, + rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined, + }); + + this._astSymbolsBySymbol.set(followedSymbol, astSymbol); + + // Okay, now while creating the declarations we will wire them up to the + // their corresponding parent declarations + for (const declaration of followedSymbol.declarations ?? []) { + let parentAstDeclaration: AstDeclaration | undefined; + if (parentAstSymbol) { + const parentDeclaration: ts.Node | undefined = this._tryFindFirstAstDeclarationParent(declaration); + + if (!parentDeclaration) { + throw new InternalError('Missing parent declaration'); + } + + parentAstDeclaration = this._astDeclarationsByDeclaration.get(parentDeclaration); + if (!parentAstDeclaration) { + throw new InternalError('Missing parent AstDeclaration'); + } + } + + const astDeclaration: AstDeclaration = new AstDeclaration({ + declaration, + astSymbol, + parent: parentAstDeclaration, + }); + + this._astDeclarationsByDeclaration.set(declaration, astDeclaration); + } + } + + if (options.isExternal !== astSymbol.isExternal) { + throw new InternalError( + `Cannot assign isExternal=${options.isExternal} for` + + ` the symbol ${astSymbol.localName} because it was previously registered` + + ` with isExternal=${astSymbol.isExternal}`, + ); + } + + return astSymbol; + } + + /** + * Returns the first parent satisfying isAstDeclaration(), or undefined if none is found. + */ + private _tryFindFirstAstDeclarationParent(node: ts.Node): ts.Node | undefined { + let currentNode: ts.Node | undefined = node.parent; + while (currentNode) { + if (AstDeclaration.isSupportedSyntaxKind(currentNode.kind)) { + return currentNode; + } + + currentNode = currentNode.parent; + } + + return undefined; + } +} diff --git a/packages/api-extractor/src/analyzer/ExportAnalyzer.ts b/packages/api-extractor/src/analyzer/ExportAnalyzer.ts new file mode 100644 index 000000000..07f53048d --- /dev/null +++ b/packages/api-extractor/src/analyzer/ExportAnalyzer.ts @@ -0,0 +1,946 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +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 { AstNamespaceImport } from './AstNamespaceImport.js'; +import { AstSymbol } from './AstSymbol.js'; +import type { IFetchAstSymbolOptions } from './AstSymbolTable.js'; +import { SourceFileLocationFormatter } from './SourceFileLocationFormatter.js'; +import { SyntaxHelpers } from './SyntaxHelpers.js'; +import { TypeScriptHelpers } from './TypeScriptHelpers.js'; +import { TypeScriptInternals } from './TypeScriptInternals.js'; + +/** + * Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer. + * + * In particular, we want ExportAnalyzer to be able to call AstSymbolTable._fetchAstSymbol() even though it + * is a very private API that should not be exposed to any other components. + */ +export interface IAstSymbolTable { + analyze(astEntity: AstEntity): void; + + fetchAstSymbol(options: IFetchAstSymbolOptions): AstSymbol | undefined; +} + +/** + * Used with ExportAnalyzer.fetchAstModuleBySourceFile() to provide contextual information about how the source file + * was imported. + */ +interface IAstModuleReference { + /** + * For example, if we are following a statement like `import { X } from 'some-package'`, this will be the + * string `"some-package"`. + */ + moduleSpecifier: string; + + /** + * For example, if we are following a statement like `import { X } from 'some-package'`, this will be the + * symbol for `X`. + */ + moduleSpecifierSymbol: ts.Symbol; +} + +/** + * The ExportAnalyzer is an internal part of AstSymbolTable that has been moved out into its own source file + * because it is a complex and mostly self-contained algorithm. + * + * Its job is to build up AstModule objects by crawling import statements to discover where declarations come from. + * This is conceptually the same as the compiler's own TypeChecker.getExportsOfModule(), except that when + * ExportAnalyzer encounters a declaration that was imported from an external package, it remembers how it was imported + * (i.e. the AstImport object). Today the compiler API does not expose this information, which is crucial for + * generating .d.ts rollups. + */ +export class ExportAnalyzer { + private readonly _program: ts.Program; + + private readonly _typeChecker: ts.TypeChecker; + + private readonly _bundledPackageNames: ReadonlySet; + + private readonly _astSymbolTable: IAstSymbolTable; + + private readonly _astModulesByModuleSymbol: Map = new Map(); + + // Used with isImportableAmbientSourceFile() + private readonly _importableAmbientSourceFiles: Set = new Set(); + + private readonly _astImportsByKey: Map = new Map(); + + private readonly _astNamespaceImportByModule: Map = new Map(); + + public constructor( + program: ts.Program, + typeChecker: ts.TypeChecker, + bundledPackageNames: ReadonlySet, + astSymbolTable: IAstSymbolTable, + ) { + this._program = program; + this._typeChecker = typeChecker; + this._bundledPackageNames = bundledPackageNames; + this._astSymbolTable = astSymbolTable; + } + + /** + * For a given source file, this analyzes all of its exports and produces an AstModule object. + * + * @param sourceFile - the sourceFile + * @param moduleReference - contextual information about the import statement that took us to this source file. + * or `undefined` if this source file is the initial entry point + * @param isExternal - whether the given `moduleReference` is external. + */ + public fetchAstModuleFromSourceFile( + sourceFile: ts.SourceFile, + moduleReference: IAstModuleReference | undefined, + isExternal: boolean, + ): AstModule { + const moduleSymbol: ts.Symbol = this._getModuleSymbolFromSourceFile(sourceFile, moduleReference); + + // Don't traverse into a module that we already processed before: + // The compiler allows m1 to have "export * from 'm2'" and "export * from 'm3'", + // even if m2 and m3 both have "export * from 'm4'". + let astModule: AstModule | undefined = this._astModulesByModuleSymbol.get(moduleSymbol); + if (!astModule) { + // (If moduleReference === undefined, then this is the entry point of the local project being analyzed.) + const externalModulePath: string | undefined = + moduleReference !== undefined && isExternal ? moduleReference.moduleSpecifier : undefined; + + astModule = new AstModule({ sourceFile, moduleSymbol, externalModulePath }); + + this._astModulesByModuleSymbol.set(moduleSymbol, astModule); + + if (astModule.isExternal) { + // It's an external package, so do the special simplified analysis that doesn't crawl into referenced modules + for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) { + if (externalModulePath === undefined) { + throw new InternalError('Failed assertion: externalModulePath=undefined but astModule.isExternal=true'); + } + + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker); + + // Ignore virtual symbols that don't have any declarations + const arbitraryDeclaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(followedSymbol); + if (arbitraryDeclaration) { + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({ + followedSymbol, + isExternal: astModule.isExternal, + includeNominalAnalysis: true, + addIfMissing: true, + }); + + if (!astSymbol) { + throw new Error( + `Unsupported export ${JSON.stringify(exportedSymbol.name)}:\n` + + SourceFileLocationFormatter.formatDeclaration(arbitraryDeclaration), + ); + } + + astModule.cachedExportedEntities.set(exportedSymbol.name, astSymbol); + } + } + } else if (moduleSymbol.exports) { + // The module is part of the local project, so do the full analysis + // The "export * from 'module-name';" declarations are all attached to a single virtual symbol + // whose name is InternalSymbolName.ExportStar + const exportStarSymbol: ts.Symbol | undefined = moduleSymbol.exports.get(ts.InternalSymbolName.ExportStar); + if (exportStarSymbol) { + for (const exportStarDeclaration of exportStarSymbol.getDeclarations() ?? []) { + if (ts.isExportDeclaration(exportStarDeclaration)) { + const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule( + exportStarDeclaration, + exportStarSymbol, + ); + + if (starExportedModule !== undefined) { + astModule.starExportedModules.add(starExportedModule); + } + } else { + // Ignore ExportDeclaration nodes that don't match the expected pattern + // Should we report a warning? + } + } + } + } + } + + return astModule; + } + + /** + * Retrieves the symbol for the module corresponding to the ts.SourceFile that is being imported/exported. + * + * @remarks + * The `module` keyword can be used to declare multiple TypeScript modules inside a single source file. + * (This is a deprecated construct and mainly used for typings such as `@types/node`.) In this situation, + * `moduleReference` helps us to fish out the correct module symbol. + */ + private _getModuleSymbolFromSourceFile( + sourceFile: ts.SourceFile, + moduleReference: IAstModuleReference | undefined, + ): ts.Symbol { + const moduleSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration( + sourceFile, + this._typeChecker, + ); + if (moduleSymbol !== undefined) { + // This is the normal case. The SourceFile acts is a module and has a symbol. + return moduleSymbol; + } + + if ( + moduleReference !== undefined && // But there is also an elaborate case where the source file contains one or more "module" declarations, + // and our moduleReference took us to one of those. + + (moduleReference.moduleSpecifierSymbol.flags & ts.SymbolFlags.Alias) !== 0 + ) { + // Follow the import/export declaration to one hop the exported item inside the target module + let followedSymbol: ts.Symbol | undefined = TypeScriptInternals.getImmediateAliasedSymbol( + moduleReference.moduleSpecifierSymbol, + this._typeChecker, + ); + + if (followedSymbol === undefined) { + // This is a workaround for a compiler bug where getImmediateAliasedSymbol() sometimes returns undefined + followedSymbol = this._typeChecker.getAliasedSymbol(moduleReference.moduleSpecifierSymbol); + } + + if (followedSymbol !== undefined && followedSymbol !== moduleReference.moduleSpecifierSymbol) { + // The parent of the exported symbol will be the module that we're importing from + const parent: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(followedSymbol); + if ( + parent !== undefined && // Make sure the thing we found is a module + (parent.flags & ts.SymbolFlags.ValueModule) !== 0 + ) { + // Record that that this is an ambient module that can also be imported from + this._importableAmbientSourceFiles.add(sourceFile); + return parent; + } + } + } + + throw new InternalError('Unable to determine module for: ' + sourceFile.fileName); + } + + /** + * Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}. + */ + public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo { + if (entryPointAstModule.isExternal) { + throw new Error('fetchAstModuleExportInfo() is not supported for external modules'); + } + + if (entryPointAstModule.astModuleExportInfo === undefined) { + const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo(); + + this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set()); + + entryPointAstModule.astModuleExportInfo = astModuleExportInfo; + } + + return entryPointAstModule.astModuleExportInfo; + } + + /** + * Returns true if the module specifier refers to an external package. Ignores packages listed in the + * "bundledPackages" setting from the api-extractor.json config file. + */ + private _isExternalModulePath( + importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode, + moduleSpecifier: string, + ): boolean { + const specifier: ts.Expression | ts.TypeNode | undefined = ts.isImportTypeNode(importOrExportDeclaration) + ? importOrExportDeclaration.argument + : importOrExportDeclaration.moduleSpecifier; + const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined = + specifier && ts.isStringLiteralLike(specifier) + ? TypeScriptInternals.getModeForUsageLocation(importOrExportDeclaration.getSourceFile(), specifier) + : undefined; + + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( + importOrExportDeclaration.getSourceFile(), + moduleSpecifier, + mode, + ); + + if (resolvedModule === undefined) { + // The TS compiler API `getResolvedModule` cannot resolve ambient modules. Thus, to match API Extractor's + // previous behavior, simply treat all ambient modules as external. This bug is tracked by + // https://github.com/microsoft/rushstack/issues/3335. + return true; + } + + // Either something like `jquery` or `@microsoft/api-extractor`. + const packageName: string | undefined = resolvedModule.packageId?.name; + if (packageName !== undefined && this._bundledPackageNames.has(packageName)) { + return false; + } + + if (resolvedModule.isExternalLibraryImport === undefined) { + // This presumably means the compiler couldn't figure out whether the module was external, but we're not + // sure how this can happen. + throw new InternalError( + `Cannot determine whether the module ${JSON.stringify(moduleSpecifier)} is external\n` + + SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration), + ); + } + + return resolvedModule.isExternalLibraryImport; + } + + /** + * Returns true if when we analyzed sourceFile, we found that it contains an "export=" statement that allows + * it to behave /either/ as an ambient module /or/ as a regular importable module. In this case, + * `AstSymbolTable._fetchAstSymbol()` will analyze its symbols even though `TypeScriptHelpers.isAmbient()` + * returns true. + */ + public isImportableAmbientSourceFile(sourceFile: ts.SourceFile): boolean { + return this._importableAmbientSourceFiles.has(sourceFile); + } + + private _collectAllExportsRecursive( + astModuleExportInfo: AstModuleExportInfo, + astModule: AstModule, + visitedAstModules: Set, + ): void { + if (visitedAstModules.has(astModule)) { + return; + } + + visitedAstModules.add(astModule); + + if (astModule.isExternal) { + astModuleExportInfo.starExportedExternalModules.add(astModule); + } else { + // Fetch each of the explicit exports for this module + if (astModule.moduleSymbol.exports) { + for (const [exportName, exportSymbol] of astModule.moduleSymbol.exports.entries()) { + switch (exportName) { + case ts.InternalSymbolName.ExportStar: + case ts.InternalSymbolName.ExportEquals: + break; + default: + // 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) + ) { + const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule); + + if (astEntity instanceof AstSymbol && !astEntity.isExternal) { + this._astSymbolTable.analyze(astEntity); + } + + if (astEntity instanceof AstNamespaceImport && !astEntity.astModule.isExternal) { + this._astSymbolTable.analyze(astEntity); + } + + astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity); + } + + break; + } + } + } + + for (const starExportedModule of astModule.starExportedModules) { + this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules); + } + } + } + + /** + * For a given symbol (which was encountered in the specified sourceFile), this fetches the AstEntity that it + * refers to. For example, if a particular interface describes the return value of a function, this API can help + * us determine a TSDoc declaration reference for that symbol (if the symbol is exported). + */ + public fetchReferencedAstEntity(symbol: ts.Symbol, referringModuleIsExternal: boolean): AstEntity | undefined { + if ((symbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0) { + // If a symbol refers back to part of its own definition, don't follow that rabbit hole + // Example: + // + // function f(x: number): typeof x { + // return 123; + // } + return undefined; + } + + let current: ts.Symbol = symbol; + + if (referringModuleIsExternal) { + current = TypeScriptHelpers.followAliases(symbol, this._typeChecker); + } else { + for (;;) { + // Is this symbol an import/export that we need to follow to find the real declaration? + for (const declaration of current.declarations ?? []) { + let matchedAstEntity: AstEntity | undefined; + matchedAstEntity = this._tryMatchExportDeclaration(declaration, current); + if (matchedAstEntity !== undefined) { + return matchedAstEntity; + } + + matchedAstEntity = this._tryMatchImportDeclaration(declaration, current); + if (matchedAstEntity !== undefined) { + return matchedAstEntity; + } + } + + if (!(current.flags & ts.SymbolFlags.Alias)) { + break; + } + + const currentAlias: ts.Symbol = TypeScriptInternals.getImmediateAliasedSymbol(current, this._typeChecker); + // Stop if we reach the end of the chain + if (!currentAlias || currentAlias === current) { + break; + } + + current = currentAlias; + } + } + + // Otherwise, assume it is a normal declaration + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({ + followedSymbol: current, + isExternal: referringModuleIsExternal, + includeNominalAnalysis: false, + addIfMissing: true, + }); + + return astSymbol; + } + + public fetchReferencedAstEntityFromImportTypeNode( + node: ts.ImportTypeNode, + referringModuleIsExternal: boolean, + ): AstEntity | undefined { + const externalModulePath: string | undefined = this._tryGetExternalModulePath(node); + + if (externalModulePath) { + let exportName: string; + if (node.qualifier) { + // Example input: + // import('api-extractor-lib1-test').Lib1GenericType + // + // Extracted qualifier: + // Lib1GenericType + exportName = node.qualifier.getText().trim(); + } else { + // Example input: + // import('api-extractor-lib1-test') + // + // Extracted qualifier: + // apiExtractorLib1Test + + exportName = SyntaxHelpers.makeCamelCaseIdentifier(externalModulePath); + } + + return this._fetchAstImport(undefined, { + importKind: AstImportKind.ImportType, + exportName, + modulePath: externalModulePath, + isTypeOnly: false, + }); + } + + // Internal reference: AstSymbol + const rightMostToken: ts.Identifier | ts.ImportTypeNode = node.qualifier + ? node.qualifier.kind === ts.SyntaxKind.QualifiedName + ? node.qualifier.right + : node.qualifier + : node; + + // There is no symbol property in a ImportTypeNode, obtain the associated export symbol + const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(rightMostToken); + if (!exportSymbol) { + throw new InternalError( + `Symbol not found for identifier: ${node.getText()}\n` + SourceFileLocationFormatter.formatDeclaration(node), + ); + } + + let followedSymbol: ts.Symbol = exportSymbol; + for (;;) { + const referencedAstEntity: AstEntity | undefined = this.fetchReferencedAstEntity( + followedSymbol, + referringModuleIsExternal, + ); + + if (referencedAstEntity) { + return referencedAstEntity; + } + + const followedSymbolNode: ts.ImportTypeNode | ts.Node | undefined = + followedSymbol.declarations && (followedSymbol.declarations[0] as ts.Node | undefined); + + if (followedSymbolNode && followedSymbolNode.kind === ts.SyntaxKind.ImportType) { + return this.fetchReferencedAstEntityFromImportTypeNode( + followedSymbolNode as ts.ImportTypeNode, + referringModuleIsExternal, + ); + } + + if (!(followedSymbol.flags & ts.SymbolFlags.Alias)) { + break; + } + + const currentAlias: ts.Symbol = this._typeChecker.getAliasedSymbol(followedSymbol); + if (!currentAlias || currentAlias === followedSymbol) { + break; + } + + followedSymbol = currentAlias; + } + + const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({ + followedSymbol, + isExternal: referringModuleIsExternal, + includeNominalAnalysis: false, + addIfMissing: true, + }); + + return astSymbol; + } + + private _tryMatchExportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined { + const exportDeclaration: ts.ExportDeclaration | undefined = TypeScriptHelpers.findFirstParent( + declaration, + ts.SyntaxKind.ExportDeclaration, + ); + + if (exportDeclaration) { + let exportName: string | undefined; + + if (declaration.kind === ts.SyntaxKind.ExportSpecifier) { + // EXAMPLE: + // "export { A } from './file-a';" + // + // ExportDeclaration: + // ExportKeyword: pre=[export] sep=[ ] + // NamedExports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ExportSpecifier: <------------- declaration + // Identifier: pre=[A] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['./file-a'] + // SemicolonToken: pre=[;] + + // Example: " ExportName as RenamedName" + const exportSpecifier: ts.ExportSpecifier = declaration as ts.ExportSpecifier; + exportName = (exportSpecifier.propertyName ?? exportSpecifier.name).getText().trim(); + } else if (declaration.kind === ts.SyntaxKind.NamespaceExport) { + // EXAMPLE: + // "export * as theLib from 'the-lib';" + // + // ExportDeclaration: + // ExportKeyword: pre=[export] sep=[ ] + // NamespaceExport: + // AsteriskToken: pre=[*] sep=[ ] + // AsKeyword: pre=[as] sep=[ ] + // Identifier: pre=[theLib] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + // Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780 + throw new Error( + `The "export * as ___" syntax is not supported yet; as a workaround,` + + ` use "import * as ___" with a separate "export { ___ }" declaration\n` + + SourceFileLocationFormatter.formatDeclaration(declaration), + ); + } else { + throw new InternalError( + `Unimplemented export declaration kind: ${declaration.getText()}\n` + + SourceFileLocationFormatter.formatDeclaration(declaration), + ); + } + + // Ignore "export { A }" without a module specifier + if (exportDeclaration.moduleSpecifier) { + const externalModulePath: string | undefined = this._tryGetExternalModulePath(exportDeclaration); + + if (externalModulePath !== undefined) { + return this._fetchAstImport(declarationSymbol, { + importKind: AstImportKind.NamedImport, + modulePath: externalModulePath, + exportName, + isTypeOnly: false, + }); + } + + return this._getExportOfSpecifierAstModule(exportName, exportDeclaration, declarationSymbol); + } + } + + return undefined; + } + + private _tryMatchImportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined { + const importDeclaration: ts.ImportDeclaration | undefined = TypeScriptHelpers.findFirstParent( + declaration, + ts.SyntaxKind.ImportDeclaration, + ); + + if (importDeclaration) { + const externalModulePath: string | undefined = this._tryGetExternalModulePath(importDeclaration); + + if (declaration.kind === ts.SyntaxKind.NamespaceImport) { + // EXAMPLE: + // "import * as theLib from 'the-lib';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: + // NamespaceImport: <------------- declaration + // AsteriskToken: pre=[*] sep=[ ] + // AsKeyword: pre=[as] sep=[ ] + // Identifier: pre=[theLib] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + 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; + } + + // 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: declarationSymbol.name, + modulePath: externalModulePath, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration), + }); + } + + if (declaration.kind === ts.SyntaxKind.ImportSpecifier) { + // EXAMPLE: + // "import { A, B } from 'the-lib';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: + // NamedImports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ImportSpecifier: <------------- declaration + // Identifier: pre=[A] + // CommaToken: pre=[,] sep=[ ] + // ImportSpecifier: + // Identifier: pre=[B] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['the-lib'] + // SemicolonToken: pre=[;] + + // Example: " ExportName as RenamedName" + const importSpecifier: ts.ImportSpecifier = declaration as ts.ImportSpecifier; + const exportName: string = (importSpecifier.propertyName ?? importSpecifier.name).getText().trim(); + + if (externalModulePath !== undefined) { + return this._fetchAstImport(declarationSymbol, { + importKind: AstImportKind.NamedImport, + modulePath: externalModulePath, + exportName, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration), + }); + } + + return this._getExportOfSpecifierAstModule(exportName, importDeclaration, declarationSymbol); + } else if (declaration.kind === ts.SyntaxKind.ImportClause) { + // EXAMPLE: + // "import A, { B } from './A';" + // + // ImportDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // ImportClause: <------------- declaration (referring to A) + // Identifier: pre=[A] + // CommaToken: pre=[,] sep=[ ] + // NamedImports: + // FirstPunctuation: pre=[{] sep=[ ] + // SyntaxList: + // ImportSpecifier: + // Identifier: pre=[B] sep=[ ] + // CloseBraceToken: pre=[}] sep=[ ] + // FromKeyword: pre=[from] sep=[ ] + // StringLiteral: pre=['./A'] + // SemicolonToken: pre=[;] + + const importClause: ts.ImportClause = declaration as ts.ImportClause; + const exportName: string = importClause.name + ? importClause.name.getText().trim() + : ts.InternalSymbolName.Default; + + if (externalModulePath !== undefined) { + return this._fetchAstImport(declarationSymbol, { + importKind: AstImportKind.DefaultImport, + modulePath: externalModulePath, + exportName, + isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration), + }); + } + + return this._getExportOfSpecifierAstModule(ts.InternalSymbolName.Default, importDeclaration, declarationSymbol); + } else { + throw new InternalError( + `Unimplemented import declaration kind: ${declaration.getText()}\n` + + SourceFileLocationFormatter.formatDeclaration(declaration), + ); + } + } + + if ( + ts.isImportEqualsDeclaration(declaration) && // EXAMPLE: + // import myLib = require('my-lib'); + // + // ImportEqualsDeclaration: + // ImportKeyword: pre=[import] sep=[ ] + // Identifier: pre=[myLib] sep=[ ] + // FirstAssignment: pre=[=] sep=[ ] + // ExternalModuleReference: + // RequireKeyword: pre=[require] + // OpenParenToken: pre=[(] + // StringLiteral: pre=['my-lib'] + // CloseParenToken: pre=[)] + // SemicolonToken: pre=[;] + ts.isExternalModuleReference(declaration.moduleReference) && + ts.isStringLiteralLike(declaration.moduleReference.expression) + ) { + const variableName: string = TypeScriptInternals.getTextOfIdentifierOrLiteral(declaration.name); + const externalModuleName: string = TypeScriptInternals.getTextOfIdentifierOrLiteral( + declaration.moduleReference.expression, + ); + + return this._fetchAstImport(declarationSymbol, { + importKind: AstImportKind.EqualsImport, + modulePath: externalModuleName, + exportName: variableName, + isTypeOnly: false, + }); + } + + return undefined; + } + + private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean { + if (importDeclaration.importClause) { + return Boolean(importDeclaration.importClause.isTypeOnly); + } + + return false; + } + + private _getExportOfSpecifierAstModule( + exportName: string, + importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration, + exportSymbol: ts.Symbol, + ): AstEntity { + const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importOrExportDeclaration, exportSymbol); + const astEntity: AstEntity = this._getExportOfAstModule(exportName, specifierAstModule); + return astEntity; + } + + private _getExportOfAstModule(exportName: string, astModule: AstModule): AstEntity { + const visitedAstModules: Set = new Set(); + const astEntity: AstEntity | undefined = this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules); + if (astEntity === undefined) { + throw new InternalError( + `Unable to analyze the export ${JSON.stringify(exportName)} in\n` + astModule.sourceFile.fileName, + ); + } + + return astEntity; + } + + /** + * Implementation of {@link AstSymbolTable.tryGetExportOfAstModule}. + */ + public tryGetExportOfAstModule(exportName: string, astModule: AstModule): AstEntity | undefined { + const visitedAstModules: Set = new Set(); + return this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules); + } + + private _tryGetExportOfAstModule( + exportName: string, + astModule: AstModule, + visitedAstModules: Set, + ): AstEntity | undefined { + if (visitedAstModules.has(astModule)) { + return undefined; + } + + visitedAstModules.add(astModule); + + let astEntity: AstEntity | undefined = astModule.cachedExportedEntities.get(exportName); + if (astEntity !== undefined) { + return astEntity; + } + + // Try the explicit exports + const escapedExportName: ts.__String = ts.escapeLeadingUnderscores(exportName); + if (astModule.moduleSymbol.exports) { + const exportSymbol: ts.Symbol | undefined = astModule.moduleSymbol.exports.get(escapedExportName); + if (exportSymbol) { + astEntity = this.fetchReferencedAstEntity(exportSymbol, astModule.isExternal); + + if (astEntity !== undefined) { + astModule.cachedExportedEntities.set(exportName, astEntity); // cache for next time + return astEntity; + } + } + } + + // Try each of the star imports + for (const starExportedModule of astModule.starExportedModules) { + astEntity = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules); + + if (astEntity !== undefined) { + if (starExportedModule.externalModulePath !== undefined) { + // This entity was obtained from an external module, so return an AstImport instead + const astSymbol: AstSymbol = astEntity as AstSymbol; + return this._fetchAstImport(astSymbol.followedSymbol, { + importKind: AstImportKind.NamedImport, + modulePath: starExportedModule.externalModulePath, + exportName, + isTypeOnly: false, + }); + } + + return astEntity; + } + } + + return undefined; + } + + private _tryGetExternalModulePath( + importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode, + ): string | undefined { + const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration); + if (this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier)) { + return moduleSpecifier; + } + + return undefined; + } + + /** + * Given an ImportDeclaration of the form `export { X } from "___";`, this interprets the module specifier (`"___"`) + * and fetches the corresponding AstModule object. + */ + private _fetchSpecifierAstModule( + importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration, + exportSymbol: ts.Symbol, + ): AstModule { + const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration); + const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined = + importOrExportDeclaration.moduleSpecifier && ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier) + ? TypeScriptInternals.getModeForUsageLocation( + importOrExportDeclaration.getSourceFile(), + importOrExportDeclaration.moduleSpecifier, + ) + : undefined; + const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule( + importOrExportDeclaration.getSourceFile(), + moduleSpecifier, + mode, + ); + + if (resolvedModule === undefined) { + // Encountered in https://github.com/microsoft/rushstack/issues/1914. + // + // It's also possible for this to occur with ambient modules. However, in practice this doesn't happen + // as API Extractor treats all ambient modules as external per the logic in `_isExternalModulePath`, and + // thus this code path is never reached for ambient modules. + throw new InternalError( + `getResolvedModule() could not resolve module name ${JSON.stringify(moduleSpecifier)}\n` + + SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration), + ); + } + + // Map the filename back to the corresponding SourceFile. This circuitous approach is needed because + // we have no way to access the compiler's internal resolveExternalModuleName() function + const moduleSourceFile: ts.SourceFile | undefined = this._program.getSourceFile(resolvedModule.resolvedFileName); + if (!moduleSourceFile) { + // This should not happen, since getResolvedModule() specifically looks up names that the compiler + // found in export declarations for this source file + throw new InternalError( + `getSourceFile() failed to locate ${JSON.stringify(resolvedModule.resolvedFileName)}\n` + + SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration), + ); + } + + const isExternal: boolean = this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier); + const moduleReference: IAstModuleReference = { + moduleSpecifier, + moduleSpecifierSymbol: exportSymbol, + }; + const specifierAstModule: AstModule = this.fetchAstModuleFromSourceFile( + moduleSourceFile, + moduleReference, + isExternal, + ); + + return specifierAstModule; + } + + private _fetchAstImport(importSymbol: ts.Symbol | undefined, options: IAstImportOptions): AstImport { + const key: string = AstImport.getKey(options); + + let astImport: AstImport | undefined = this._astImportsByKey.get(key); + + if (astImport) { + // If we encounter at least one import that does not use the type-only form, + // then the .d.ts rollup will NOT use "import type". + if (!options.isTypeOnly) { + astImport.isTypeOnlyEverywhere = false; + } + } else { + astImport = new AstImport(options); + this._astImportsByKey.set(key, astImport); + + if (importSymbol) { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(importSymbol, this._typeChecker); + + astImport.astSymbol = this._astSymbolTable.fetchAstSymbol({ + followedSymbol, + isExternal: true, + includeNominalAnalysis: false, + addIfMissing: true, + }); + } + } + + return astImport; + } + + private _getModuleSpecifier( + importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode, + ): string { + // The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point' + const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(importOrExportDeclaration); + + if (!moduleSpecifier) { + throw new InternalError( + 'Unable to parse module specifier\n' + SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration), + ); + } + + return moduleSpecifier; + } +} diff --git a/packages/api-extractor/src/analyzer/PackageMetadataManager.ts b/packages/api-extractor/src/analyzer/PackageMetadataManager.ts new file mode 100644 index 000000000..26d52bd06 --- /dev/null +++ b/packages/api-extractor/src/analyzer/PackageMetadataManager.ts @@ -0,0 +1,201 @@ +// 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 { + type PackageJsonLookup, + FileSystem, + JsonFile, + type NewlineKind, + type INodePackageJson, + type JsonObject, +} from '@rushstack/node-core-library'; +import { ConsoleMessageId } from '../api/ConsoleMessageId.js'; +import { Extractor } from '../api/Extractor.js'; +import type { MessageRouter } from '../collector/MessageRouter.js'; + +/** + * Represents analyzed information for a package.json file. + * This object is constructed and returned by PackageMetadataManager. + */ +export class PackageMetadata { + /** + * The absolute path to the package.json file being analyzed. + */ + public readonly packageJsonPath: string; + + /** + * The parsed contents of package.json. Note that PackageJsonLookup + * only includes essential fields. + */ + public readonly packageJson: INodePackageJson; + + /** + * If true, then the package's documentation comments can be assumed + * to contain API Extractor compatible TSDoc tags. + */ + public readonly aedocSupported: boolean; + + public constructor(packageJsonPath: string, packageJson: INodePackageJson, aedocSupported: boolean) { + this.packageJsonPath = packageJsonPath; + this.packageJson = packageJson; + this.aedocSupported = aedocSupported; + } +} + +/** + * This class maintains a cache of analyzed information obtained from package.json + * files. It is built on top of the PackageJsonLookup class. + * + * @remarks + * + * IMPORTANT: Don't use PackageMetadataManager to analyze source files from the current project: + * 1. Files such as tsdoc-metadata.json may not have been built yet, and thus may contain incorrect information. + * 2. The current project is not guaranteed to have a package.json file at all. For example, API Extractor can + * be invoked on a bare .d.ts file. + * + * Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager. + */ +export class PackageMetadataManager { + public static tsdocMetadataFilename: string = 'tsdoc-metadata.json'; + + private readonly _packageJsonLookup: PackageJsonLookup; + + private readonly _messageRouter: MessageRouter; + + private readonly _packageMetadataByPackageJsonPath: Map = new Map(); + + public constructor(packageJsonLookup: PackageJsonLookup, messageRouter: MessageRouter) { + this._packageJsonLookup = packageJsonLookup; + 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. + 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; + } + + // Always resolve relative to the package folder. + const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath); + return tsdocMetadataPath; + } + + /** + * @param packageFolder - The package folder + * @param packageJson - The package JSON + * @param tsdocMetadataPath - An explicit path that can be configured in api-extractor.json. + * If this parameter is not an empty string, it overrides the normal path calculation. + * @returns the absolute path to the TSDoc metadata file + */ + public static resolveTsdocMetadataPath( + packageFolder: string, + packageJson: INodePackageJson, + tsdocMetadataPath?: string, + ): string { + if (tsdocMetadataPath) { + return path.resolve(packageFolder, tsdocMetadataPath); + } + + return PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(packageFolder, packageJson); + } + + /** + * Writes the TSDoc metadata file to the specified output file. + */ + public static writeTsdocMetadataFile(tsdocMetadataPath: string, newlineKind: NewlineKind): void { + const fileObject: JsonObject = { + tsdocVersion: '0.12', + toolPackages: [ + { + packageName: '@microsoft/api-extractor', + packageVersion: Extractor.version, + }, + ], + }; + + const fileContent: string = + '// This file is read by tools that parse documentation comments conforming to the TSDoc standard.\n' + + '// It should be published with your NPM package. It should not be tracked by Git.\n' + + JsonFile.stringify(fileObject); + + FileSystem.writeFile(tsdocMetadataPath, fileContent, { + convertLineEndings: newlineKind, + ensureFolderExists: true, + }); + } + + /** + * Finds the package.json in a parent folder of the specified source file, and + * returns a PackageMetadata object. If no package.json was found, then undefined + * is returned. The results are cached. + */ + public tryFetchPackageMetadata(sourceFilePath: string): PackageMetadata | undefined { + const packageJsonFilePath: string | undefined = + this._packageJsonLookup.tryGetPackageJsonFilePathFor(sourceFilePath); + if (!packageJsonFilePath) { + return undefined; + } + + let packageMetadata: PackageMetadata | undefined = this._packageMetadataByPackageJsonPath.get(packageJsonFilePath); + + if (!packageMetadata) { + const packageJson: INodePackageJson = this._packageJsonLookup.loadNodePackageJson(packageJsonFilePath); + + const packageJsonFolder: string = path.dirname(packageJsonFilePath); + + let aedocSupported = false; + + const tsdocMetadataPath: string = PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson( + packageJsonFolder, + packageJson, + ); + + if (FileSystem.exists(tsdocMetadataPath)) { + this._messageRouter.logVerbose(ConsoleMessageId.FoundTSDocMetadata, 'Found metadata in ' + tsdocMetadataPath); + // If the file exists at all, assume it was written by API Extractor + aedocSupported = true; + } + + packageMetadata = new PackageMetadata(packageJsonFilePath, packageJson, aedocSupported); + this._packageMetadataByPackageJsonPath.set(packageJsonFilePath, packageMetadata); + } + + return packageMetadata; + } + + /** + * Returns true if the source file is part of a package whose .d.ts files support AEDoc annotations. + */ + public isAedocSupportedFor(sourceFilePath: string): boolean { + const packageMetadata: PackageMetadata | undefined = this.tryFetchPackageMetadata(sourceFilePath); + if (!packageMetadata) { + return false; + } + + return packageMetadata.aedocSupported; + } +} diff --git a/packages/api-extractor/src/analyzer/SourceFileLocationFormatter.ts b/packages/api-extractor/src/analyzer/SourceFileLocationFormatter.ts new file mode 100644 index 000000000..0a4a49838 --- /dev/null +++ b/packages/api-extractor/src/analyzer/SourceFileLocationFormatter.ts @@ -0,0 +1,59 @@ +// 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, Text } from '@rushstack/node-core-library'; +import type * as ts from 'typescript'; + +export interface ISourceFileLocationFormatOptions { + sourceFileColumn?: number | undefined; + sourceFileLine?: number | undefined; + workingPackageFolderPath?: string | undefined; +} + +export class SourceFileLocationFormatter { + /** + * Returns a string such as this, based on the context information in the provided node: + * "[C:\\Folder\\File.ts#123]" + */ + public static formatDeclaration(node: ts.Node, workingPackageFolderPath?: string): string { + const sourceFile: ts.SourceFile = node.getSourceFile(); + const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + + return SourceFileLocationFormatter.formatPath(sourceFile.fileName, { + sourceFileLine: lineAndCharacter.line + 1, + sourceFileColumn: lineAndCharacter.character + 1, + workingPackageFolderPath, + }); + } + + public static formatPath(sourceFilePath: string, options?: ISourceFileLocationFormatOptions): string { + const ioptions = options ?? {}; + + let result = ''; + + // Make the path relative to the workingPackageFolderPath + let scrubbedPath: string = sourceFilePath; + + if ( + ioptions.workingPackageFolderPath && // If it's under the working folder, make it a relative path + Path.isUnderOrEqual(sourceFilePath, ioptions.workingPackageFolderPath) + ) { + scrubbedPath = path.relative(ioptions.workingPackageFolderPath, sourceFilePath); + } + + // Convert it to a Unix-style path + scrubbedPath = Text.replaceAll(scrubbedPath, '\\', '/'); + result += scrubbedPath; + + if (ioptions.sourceFileLine) { + result += `:${ioptions.sourceFileLine}`; + + if (ioptions.sourceFileColumn) { + result += `:${ioptions.sourceFileColumn}`; + } + } + + return result; + } +} diff --git a/packages/api-extractor/src/analyzer/Span.ts b/packages/api-extractor/src/analyzer/Span.ts new file mode 100644 index 000000000..dc94a08d2 --- /dev/null +++ b/packages/api-extractor/src/analyzer/Span.ts @@ -0,0 +1,683 @@ +/* eslint-disable promise/prefer-await-to-callbacks */ +// 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 * as ts from 'typescript'; +import { IndentedWriter } from '../generators/IndentedWriter.js'; + +interface IWriteModifiedTextOptions { + indentDocCommentState: IndentDocCommentState; + separatorOverride: string | undefined; + writer: IndentedWriter; +} + +enum IndentDocCommentState { + /** + * `indentDocComment` was not requested for this subtree. + */ + Inactive = 0, + /** + * `indentDocComment` was requested and we are looking for the opening `/` `*` + */ + AwaitingOpenDelimiter = 1, + /** + * `indentDocComment` was requested and we are looking for the closing `*` `/` + */ + AwaitingCloseDelimiter = 2, + /** + * `indentDocComment` was requested and we have finished indenting the comment. + */ + Done = 3, +} + +/** + * Choices for SpanModification.indentDocComment. + */ +export enum IndentDocCommentScope { + /** + * Do not detect and indent comments. + */ + None = 0, + + /** + * Look for one doc comment in the {@link Span.prefix} text only. + */ + PrefixOnly = 1, + + /** + * Look for one doc comment potentially distributed across the Span and its children. + */ + SpanAndChildren = 2, +} + +/** + * Specifies various transformations that will be performed by Span.getModifiedText(). + */ +export class SpanModification { + /** + * If true, all of the child spans will be omitted from the Span.getModifiedText() output. + * + * @remarks + * Also, the modify() operation will not recurse into these spans. + */ + public omitChildren: boolean = false; + + /** + * If true, then the Span.separator will be removed from the Span.getModifiedText() output. + */ + public omitSeparatorAfter: boolean = false; + + /** + * If true, then Span.getModifiedText() will sort the immediate children according to their Span.sortKey + * property. The separators will also be fixed up to ensure correct indentation. If the Span.sortKey is undefined + * for some items, those items will not be moved, i.e. their array indexes will be unchanged. + */ + public sortChildren: boolean = false; + + /** + * Used if the parent span has Span.sortChildren=true. + */ + public sortKey: string | undefined; + + /** + * Optionally configures getModifiedText() to search for a "/*" doc comment and indent it. + * At most one comment is detected. + * + * @remarks + * The indentation can be applied to the `Span.modifier.prefix` only, or it can be applied to the + * full subtree of nodes (as needed for `ts.SyntaxKind.JSDocComment` trees). However the enabled + * scopes must not overlap. + * + * This feature is enabled selectively because (1) we do not want to accidentally match `/*` appearing + * in a string literal or other expression that is not a comment, and (2) parsing comments is relatively + * expensive. + */ + public indentDocComment: IndentDocCommentScope = IndentDocCommentScope.None; + + private readonly _span: Span; + + private _prefix: string | undefined; + + private _suffix: string | undefined; + + public constructor(span: Span) { + this._span = span; + this.reset(); + } + + /** + * Allows the Span.prefix text to be changed. + */ + public get prefix(): string { + return this._prefix ?? this._span.prefix; + } + + public set prefix(value: string) { + this._prefix = value; + } + + /** + * Allows the Span.suffix text to be changed. + */ + public get suffix(): string { + return this._suffix ?? this._span.suffix; + } + + public set suffix(value: string) { + this._suffix = value; + } + + /** + * Reverts any modifications made to this object. + */ + public reset(): void { + this.omitChildren = false; + this.omitSeparatorAfter = false; + this.sortChildren = false; + this.sortKey = undefined; + this._prefix = undefined; + this._suffix = undefined; + if (this._span.kind === ts.SyntaxKind.JSDocComment) { + this.indentDocComment = IndentDocCommentScope.SpanAndChildren; + } + } + + /** + * Effectively deletes the Span from the tree, by skipping its children, skipping its separator, + * and setting its prefix/suffix to the empty string. + */ + public skipAll(): void { + this.prefix = ''; + this.suffix = ''; + this.omitChildren = true; + this.omitSeparatorAfter = true; + } +} + +/** + * The Span class provides a simple way to rewrite TypeScript source files + * based on simple syntax transformations, i.e. without having to process deeper aspects + * of the underlying grammar. An example transformation might be deleting JSDoc comments + * from a source file. + * + * @remarks + * TypeScript's abstract syntax tree (AST) is represented using Node objects. + * The Node text ignores its surrounding whitespace, and does not have an ordering guarantee. + * For example, a JSDocComment node can be a child of a FunctionDeclaration node, even though + * the actual comment precedes the function in the input stream. + * + * The Span class is a wrapper for a single Node, that provides access to every character + * in the input stream, such that Span.getText() will exactly reproduce the corresponding + * full Node.getText() output. + * + * A Span is comprised of these parts, which appear in sequential order: + * - A prefix + * - A collection of child spans + * - A suffix + * - A separator (e.g. whitespace between this span and the next item in the tree) + * + * These parts can be modified via Span.modification. The modification is applied by + * calling Span.getModifiedText(). + */ +export class Span { + public readonly node: ts.Node; + + // To improve performance, substrings are not allocated until actually needed + public readonly startIndex: number; + + public readonly endIndex: number; + + public readonly children: Span[]; + + public readonly modification: SpanModification; + + private readonly _parent: Span | undefined; + + private readonly _previousSibling: Span | undefined; + + private readonly _nextSibling: Span | undefined; + + private readonly _separatorStartIndex: number; + + private readonly _separatorEndIndex: number; + + public constructor(node: ts.Node) { + this.node = node; + this.startIndex = node.kind === ts.SyntaxKind.SourceFile ? node.getFullStart() : node.getStart(); + this.endIndex = node.end; + this._separatorStartIndex = 0; + this._separatorEndIndex = 0; + this.children = []; + this.modification = new SpanModification(this); + + let previousChildSpan: Span | undefined; + + for (const childNode of this.node.getChildren() || []) { + const childSpan: Span = new Span(childNode); + // @ts-expect-error assigning private readonly properties on creation only + childSpan._parent = this; + // @ts-expect-error assigning private readonly properties on creation only + childSpan._previousSibling = previousChildSpan; + + if (previousChildSpan) { + // @ts-expect-error assigning private readonly properties on creation only + previousChildSpan._nextSibling = childSpan; + } + + this.children.push(childSpan); + + // Normalize the bounds so that a child is never outside its parent + if (childSpan.startIndex < this.startIndex) { + this.startIndex = childSpan.startIndex; + } + + if (childSpan.endIndex > this.endIndex) { + // This has never been observed empirically, but here's how we would handle it + this.endIndex = childSpan.endIndex; + throw new InternalError('Unexpected AST case'); + } + + if (previousChildSpan && previousChildSpan.endIndex < childSpan.startIndex) { + // There is some leftover text after previous child -- assign it as the separator for + // the preceding span. If the preceding span has no suffix, then assign it to the + // deepest preceding span with no suffix. This heuristic simplifies the most + // common transformations, and otherwise it can be fished out using getLastInnerSeparator(). + let separatorRecipient: Span = previousChildSpan; + while (separatorRecipient.children.length > 0) { + const lastChild: Span = separatorRecipient.children[separatorRecipient.children.length - 1]!; + if (lastChild.endIndex !== separatorRecipient.endIndex) { + // There is a suffix, so we cannot push the separator any further down, or else + // it would get printed before this suffix. + break; + } + + separatorRecipient = lastChild; + } + + // @ts-expect-error assigning private readonly properties on creation only + separatorRecipient._separatorStartIndex = previousChildSpan.endIndex; + // @ts-expect-error assigning private readonly properties on creation only + separatorRecipient._separatorEndIndex = childSpan.startIndex; + } + + previousChildSpan = childSpan; + } + } + + public get kind(): ts.SyntaxKind { + return this.node.kind; + } + + /** + * The parent Span, if any. + * NOTE: This will be undefined for a root Span, even though the corresponding Node + * may have a parent in the AST. + */ + public get parent(): Span | undefined { + return this._parent; + } + + /** + * If the current object is this.parent.children[i], then previousSibling corresponds + * to this.parent.children[i-1] if it exists. + * NOTE: This will be undefined for a root Span, even though the corresponding Node + * may have a previous sibling in the AST. + */ + public get previousSibling(): Span | undefined { + return this._previousSibling; + } + + /** + * If the current object is this.parent.children[i], then previousSibling corresponds + * to this.parent.children[i+1] if it exists. + * NOTE: This will be undefined for a root Span, even though the corresponding Node + * may have a previous sibling in the AST. + */ + public get nextSibling(): Span | undefined { + return this._nextSibling; + } + + /** + * The text associated with the underlying Node, up to its first child. + */ + public get prefix(): string { + if (this.children.length) { + // Everything up to the first child + return this._getSubstring(this.startIndex, this.children[0]!.startIndex); + } else { + return this._getSubstring(this.startIndex, this.endIndex); + } + } + + /** + * The text associated with the underlying Node, after its last child. + * If there are no children, this is always an empty string. + */ + public get suffix(): string { + if (this.children.length) { + // Everything after the last child + return this._getSubstring(this.children[this.children.length - 1]!.endIndex, this.endIndex); + } else { + return ''; + } + } + + /** + * Whitespace that appeared after this node, and before the "next" node in the tree. + * Here we mean "next" according to an inorder traversal, not necessarily a sibling. + */ + public get separator(): string { + return this._getSubstring(this._separatorStartIndex, this._separatorEndIndex); + } + + /** + * Returns the separator of this Span, or else recursively calls getLastInnerSeparator() + * on the last child. + */ + public getLastInnerSeparator(): string { + if (this.separator) { + return this.separator; + } + + if (this.children.length > 0) { + return this.children[this.children.length - 1]!.getLastInnerSeparator(); + } + + return ''; + } + + /** + * Returns the first parent node with the specified SyntaxKind, or undefined if there is no match. + */ + public findFirstParent(kindToMatch: ts.SyntaxKind): Span | undefined { + let current: Span | undefined = this; + + while (current) { + if (current.kind === kindToMatch) { + return current; + } + + current = current.parent; + } + + return undefined; + } + + /** + * Recursively invokes the callback on this Span and all its children. The callback + * can make changes to Span.modification for each node. + */ + public forEach(callback: (span: Span) => void): void { + // eslint-disable-next-line n/callback-return, n/no-callback-literal + callback(this); + for (const child of this.children) { + // eslint-disable-next-line unicorn/no-array-for-each + child.forEach(callback); + } + } + + /** + * Returns the original unmodified text represented by this Span. + */ + public getText(): string { + let result = ''; + result += this.prefix; + + for (const child of this.children) { + result += child.getText(); + } + + result += this.suffix; + result += this.separator; + + return result; + } + + /** + * Returns the text represented by this Span, after applying all requested modifications. + */ + public getModifiedText(): string { + const writer: IndentedWriter = new IndentedWriter(); + writer.trimLeadingSpaces = true; + + this._writeModifiedText({ + writer, + separatorOverride: undefined, + indentDocCommentState: IndentDocCommentState.Inactive, + }); + + return writer.getText(); + } + + public writeModifiedText(output: IndentedWriter): void { + this._writeModifiedText({ + writer: output, + separatorOverride: undefined, + indentDocCommentState: IndentDocCommentState.Inactive, + }); + } + + /** + * Returns a diagnostic dump of the tree, showing the prefix/suffix/separator for + * each node. + */ + public getDump(indent: string = ''): string { + let result: string = indent + ts.SyntaxKind[this.node.kind] + ': '; + + if (this.prefix) { + result += ' pre=[' + this._getTrimmed(this.prefix) + ']'; + } + + if (this.suffix) { + result += ' suf=[' + this._getTrimmed(this.suffix) + ']'; + } + + if (this.separator) { + result += ' sep=[' + this._getTrimmed(this.separator) + ']'; + } + + result += '\n'; + + for (const child of this.children) { + result += child.getDump(indent + ' '); + } + + return result; + } + + /** + * Returns a diagnostic dump of the tree, showing the SpanModification settings for each nodde. + */ + public getModifiedDump(indent: string = ''): string { + let result: string = indent + ts.SyntaxKind[this.node.kind] + ': '; + + if (this.prefix) { + result += ' pre=[' + this._getTrimmed(this.modification.prefix) + ']'; + } + + if (this.suffix) { + result += ' suf=[' + this._getTrimmed(this.modification.suffix) + ']'; + } + + if (this.separator) { + result += ' sep=[' + this._getTrimmed(this.separator) + ']'; + } + + if (this.modification.indentDocComment !== IndentDocCommentScope.None) { + result += ' indentDocComment=' + IndentDocCommentScope[this.modification.indentDocComment]; + } + + if (this.modification.omitChildren) { + result += ' omitChildren'; + } + + if (this.modification.omitSeparatorAfter) { + result += ' omitSeparatorAfter'; + } + + if (this.modification.sortChildren) { + result += ' sortChildren'; + } + + if (this.modification.sortKey !== undefined) { + result += ` sortKey="${this.modification.sortKey}"`; + } + + result += '\n'; + + if (this.modification.omitChildren) { + result += `${indent} (${this.children.length} children)\n`; + } else { + for (const child of this.children) { + result += child.getModifiedDump(indent + ' '); + } + } + + return result; + } + + /** + * Recursive implementation of `getModifiedText()` and `writeModifiedText()`. + */ + private _writeModifiedText(options: IWriteModifiedTextOptions): void { + // Apply indentation based on "{" and "}" + if (this.prefix === '{') { + options.writer.increaseIndent(); + } else if (this.prefix === '}') { + options.writer.decreaseIndent(); + } + + if (this.modification.indentDocComment !== IndentDocCommentScope.None) { + this._beginIndentDocComment(options); + } + + this._write(this.modification.prefix, options); + + if (this.modification.indentDocComment === IndentDocCommentScope.PrefixOnly) { + this._endIndentDocComment(options); + } + + let sortedSubset: Span[] | undefined; + + if (!this.modification.omitChildren && this.modification.sortChildren) { + // We will only sort the items with a sortKey + const filtered: Span[] = this.children.filter((x) => x.modification.sortKey !== undefined); + + // Is there at least one of them? + if (filtered.length > 1) { + sortedSubset = filtered; + } + } + + if (sortedSubset) { + // This is the complicated special case that sorts an arbitrary subset of the child nodes, + // preserving the surrounding nodes. + + const sortedSubsetCount: number = sortedSubset.length; + // Remember the separator for the first and last ones + const firstSeparator: string = sortedSubset[0]!.getLastInnerSeparator(); + const lastSeparator: string = sortedSubset[sortedSubsetCount - 1]!.getLastInnerSeparator(); + + Sort.sortBy(sortedSubset, (x) => x.modification.sortKey); + + const childOptions: IWriteModifiedTextOptions = { ...options }; + + let sortedSubsetIndex = 0; + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < this.children.length; ++index) { + let current: Span; + + // Is this an item that we sorted? + if (this.children[index]!.modification.sortKey === undefined) { + // No, take the next item from the original array + current = this.children[index]!; + childOptions.separatorOverride = undefined; + } else { + // Yes, take the next item from the sortedSubset + current = sortedSubset[sortedSubsetIndex++]!; + + if (sortedSubsetIndex < sortedSubsetCount) { + childOptions.separatorOverride = firstSeparator; + } else { + childOptions.separatorOverride = lastSeparator; + } + } + + current._writeModifiedText(childOptions); + } + } else { + // This is the normal case that does not need to sort children + const childrenLength: number = this.children.length; + + if (!this.modification.omitChildren) { + if (options.separatorOverride === undefined) { + // The normal simple case + for (const child of this.children) { + child._writeModifiedText(options); + } + } else { + // Special case where the separatorOverride is passed down to the "last inner separator" span + for (let index = 0; index < childrenLength; ++index) { + const child: Span = this.children[index]!; + + if ( + // Only the last child inherits the separatorOverride, because only it can contain + // the "last inner separator" span + index < childrenLength - 1 || + // If this.separator is specified, then we will write separatorOverride below, so don't pass it along + this.separator + ) { + const childOptions: IWriteModifiedTextOptions = { ...options }; + childOptions.separatorOverride = undefined; + child._writeModifiedText(childOptions); + } else { + child._writeModifiedText(options); + } + } + } + } + + this._write(this.modification.suffix, options); + + if (options.separatorOverride !== undefined) { + if (this.separator || childrenLength === 0) { + this._write(options.separatorOverride, options); + } + } else if (!this.modification.omitSeparatorAfter) { + this._write(this.separator, options); + } + } + + if (this.modification.indentDocComment === IndentDocCommentScope.SpanAndChildren) { + this._endIndentDocComment(options); + } + } + + private _beginIndentDocComment(options: IWriteModifiedTextOptions): void { + if (options.indentDocCommentState !== IndentDocCommentState.Inactive) { + throw new InternalError('indentDocComment cannot be nested'); + } + + options.indentDocCommentState = IndentDocCommentState.AwaitingOpenDelimiter; + } + + private _endIndentDocComment(options: IWriteModifiedTextOptions): void { + if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) { + throw new InternalError('missing "*/" delimiter for comment block'); + } + + options.indentDocCommentState = IndentDocCommentState.Inactive; + } + + /** + * Writes one chunk of `text` to the `options.writer`, applying the `indentDocComment` rewriting. + */ + private _write(text: string, options: IWriteModifiedTextOptions): void { + let parsedText: string = text; + + if (options.indentDocCommentState === IndentDocCommentState.AwaitingOpenDelimiter) { + let index: number = parsedText.indexOf('/*'); + if (index >= 0) { + index += '/*'.length; + options.writer.write(parsedText.slice(0, Math.max(0, index))); + parsedText = parsedText.slice(Math.max(0, index)); + options.indentDocCommentState = IndentDocCommentState.AwaitingCloseDelimiter; + + options.writer.increaseIndent(' '); + } + } + + if (options.indentDocCommentState === IndentDocCommentState.AwaitingCloseDelimiter) { + let index: number = parsedText.indexOf('*/'); + if (index >= 0) { + index += '*/'.length; + options.writer.write(parsedText.slice(0, Math.max(0, index))); + parsedText = parsedText.slice(Math.max(0, index)); + options.indentDocCommentState = IndentDocCommentState.Done; + + options.writer.decreaseIndent(); + } + } + + options.writer.write(parsedText); + } + + 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; + } + + private _getSubstring(startIndex: number, endIndex: number): string { + if (startIndex === endIndex) { + return ''; + } + + return this.node.getSourceFile().text.slice(startIndex, endIndex); + } +} diff --git a/packages/api-extractor/src/analyzer/SyntaxHelpers.ts b/packages/api-extractor/src/analyzer/SyntaxHelpers.ts new file mode 100644 index 000000000..0d792df1b --- /dev/null +++ b/packages/api-extractor/src/analyzer/SyntaxHelpers.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as ts from 'typescript'; + +/** + * Helpers for validating various text string formats. + */ +export class SyntaxHelpers { + /** + * Tests whether the input string is safe to use as an ECMAScript identifier without quotes. + * + * @remarks + * For example: + * + * ```ts + * class X { + * public okay: number = 1; + * public "not okay!": number = 2; + * } + * ``` + * + * A precise check is extremely complicated and highly dependent on the ECMAScript standard version + * and how faithfully the interpreter implements it. To keep things simple, `isSafeUnquotedMemberIdentifier()` + * conservatively accepts any identifier that would be valid with ECMAScript 5, and returns false otherwise. + */ + public static isSafeUnquotedMemberIdentifier(identifier: string): boolean { + if (identifier.length === 0) { + return false; // cannot be empty + } + + if (!ts.isIdentifierStart(identifier.codePointAt(0)!, ts.ScriptTarget.ES5)) { + return false; + } + + for (let index = 1; index < identifier.length; index++) { + if (!ts.isIdentifierPart(identifier.codePointAt(index)!, ts.ScriptTarget.ES5)) { + return false; + } + } + + return true; + } + + /** + * Given an arbitrary input string, return a regular TypeScript identifier name. + * + * @remarks + * Example input: "api-extractor-lib1-test" + * Example output: "apiExtractorLib1Test" + */ + public static makeCamelCaseIdentifier(input: string): string { + const parts: string[] = input.split(/\W+/).filter((x) => x.length > 0); + if (parts.length === 0) { + return '_'; + } + + for (let index = 0; index < parts.length; ++index) { + let part: string = parts[index]!; + if (part.toUpperCase() === part) { + // Preserve existing case unless the part is all upper-case + part = part.toLowerCase(); + } + + if (index === 0) { + // If the first part starts with a number, prepend "_" + if (/\d/.test(part.charAt(0))) { + part = '_' + part; + } + } else { + // Capitalize the first letter of each part, except for the first one + part = part.charAt(0).toUpperCase() + part.slice(1); + } + + parts[index] = part; + } + + return parts.join(''); + } +} diff --git a/packages/api-extractor/src/analyzer/TypeScriptHelpers.ts b/packages/api-extractor/src/analyzer/TypeScriptHelpers.ts new file mode 100644 index 000000000..b3d8ce670 --- /dev/null +++ b/packages/api-extractor/src/analyzer/TypeScriptHelpers.ts @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import { SourceFileLocationFormatter } from './SourceFileLocationFormatter.js'; +import { TypeScriptInternals } from './TypeScriptInternals.js'; + +export class TypeScriptHelpers { + // Matches TypeScript's encoded names for well-known ECMAScript symbols like + // "__@iterator" or "__@toStringTag". + private static readonly _wellKnownSymbolNameRegExp: RegExp = /^__@(?\w+)$/; + + // Matches TypeScript's encoded names for late-bound symbols derived from `unique symbol` declarations + // which have the form of "__@@", i.e. "__@someSymbol@12345". + private static readonly _uniqueSymbolNameRegExp: RegExp = /^__@.*@\d+$/; + + /** + * This traverses any symbol aliases to find the original place where an item was defined. + * For example, suppose a class is defined as "export default class MyClass \{ \}" + * but exported from the package's index.ts like this: + * + * export \{ default as _MyClass \} from './MyClass'; + * + * In this example, calling followAliases() on the _MyClass symbol will return the + * original definition of MyClass, traversing any intermediary places where the + * symbol was imported and re-exported. + */ + public static followAliases(symbol: ts.Symbol, typeChecker: ts.TypeChecker): ts.Symbol { + let current: ts.Symbol = symbol; + for (;;) { + if (!(current.flags & ts.SymbolFlags.Alias)) { + break; + } + + const currentAlias: ts.Symbol = typeChecker.getAliasedSymbol(current); + if (!currentAlias || currentAlias === current) { + break; + } + + current = currentAlias; + } + + return current; + } + + /** + * Returns true if TypeScriptHelpers.followAliases() would return something different + * from the input `symbol`. + */ + public static isFollowableAlias(symbol: ts.Symbol, typeChecker: ts.TypeChecker): boolean { + if (!(symbol.flags & ts.SymbolFlags.Alias)) { + return false; + } + + const alias: ts.Symbol = typeChecker.getAliasedSymbol(symbol); + + return alias && alias !== symbol; + } + + /** + * Certain virtual symbols do not have any declarations. For example, `ts.TypeChecker.getExportsOfModule()` can + * sometimes return a "prototype" symbol for an object, even though there is no corresponding declaration in the + * source code. API Extractor generally ignores such symbols. + */ + public static tryGetADeclaration(symbol: ts.Symbol): ts.Declaration | undefined { + if (symbol.declarations && symbol.declarations.length > 0) { + return symbol.declarations[0]; + } + + return undefined; + } + + /** + * Returns true if the specified symbol is an ambient declaration. + */ + public static isAmbient(symbol: ts.Symbol, typeChecker: ts.TypeChecker): boolean { + const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(symbol, typeChecker); + + if (followedSymbol.declarations && followedSymbol.declarations.length > 0) { + const firstDeclaration: ts.Declaration = followedSymbol.declarations[0]!; + + // Test 1: Are we inside the sinister "declare global {" construct? + const highestModuleDeclaration: ts.ModuleDeclaration | undefined = TypeScriptHelpers.findHighestParent( + firstDeclaration, + ts.SyntaxKind.ModuleDeclaration, + ); + if (highestModuleDeclaration && highestModuleDeclaration.name.getText().trim() === 'global') { + return true; + } + + // Test 2: Otherwise, the main heuristic for ambient declarations is by looking at the + // ts.SyntaxKind.SourceFile node to see whether it has a symbol or not (i.e. whether it + // is acting as a module or not). + const sourceFile: ts.SourceFile = firstDeclaration.getSourceFile(); + + if (typeChecker.getSymbolAtLocation(sourceFile)) { + return false; + } + } + + return true; + } + + /** + * Same semantics as tryGetSymbolForDeclaration(), but throws an exception if the symbol + * cannot be found. + */ + public static getSymbolForDeclaration(declaration: ts.Declaration, checker: ts.TypeChecker): ts.Symbol { + const symbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(declaration, checker); + if (!symbol) { + throw new InternalError( + 'Unable to determine semantic information for declaration:\n' + + SourceFileLocationFormatter.formatDeclaration(declaration), + ); + } + + return symbol; + } + + // Return name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point' + public static getModuleSpecifier( + nodeWithModuleSpecifier: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode, + ): string | undefined { + if (nodeWithModuleSpecifier.kind === ts.SyntaxKind.ImportType) { + // As specified internally in typescript:/src/compiler/types.ts#ValidImportTypeNode + if ( + nodeWithModuleSpecifier.argument.kind !== ts.SyntaxKind.LiteralType || + (nodeWithModuleSpecifier.argument as ts.LiteralTypeNode).literal.kind !== ts.SyntaxKind.StringLiteral + ) { + throw new InternalError( + `Invalid ImportTypeNode: ${nodeWithModuleSpecifier.getText()}\n` + + SourceFileLocationFormatter.formatDeclaration(nodeWithModuleSpecifier), + ); + } + + const literalTypeNode: ts.LiteralTypeNode = nodeWithModuleSpecifier.argument as ts.LiteralTypeNode; + const stringLiteral: ts.StringLiteral = literalTypeNode.literal as ts.StringLiteral; + return stringLiteral.text.trim(); + } + + // Node is a declaration + if (nodeWithModuleSpecifier.moduleSpecifier && ts.isStringLiteralLike(nodeWithModuleSpecifier.moduleSpecifier)) { + return TypeScriptInternals.getTextOfIdentifierOrLiteral(nodeWithModuleSpecifier.moduleSpecifier); + } + + return undefined; + } + + /** + * Returns an ancestor of "node", such that the ancestor, any intermediary nodes, + * and the starting node match a list of expected kinds. Undefined is returned + * if there aren't enough ancestors, or if the kinds are incorrect. + * + * For example, suppose child "C" has parents A --\> B --\> C. + * + * Calling _matchAncestor(C, [ExportSpecifier, NamedExports, ExportDeclaration]) + * would return A only if A is of kind ExportSpecifier, B is of kind NamedExports, + * and C is of kind ExportDeclaration. + * + * Calling _matchAncestor(C, [ExportDeclaration]) would return C. + */ + public static matchAncestor(node: ts.Node, kindsToMatch: ts.SyntaxKind[]): T | undefined { + // (slice(0) clones an array) + const reversedParentKinds: ts.SyntaxKind[] = kindsToMatch.slice(0).reverse(); + + let current: ts.Node | undefined; + + for (const parentKind of reversedParentKinds) { + if (current) { + // Then walk the parents + current = current.parent; + } else { + // The first time through, start with node + current = node; + } + + // If we ran out of items, or if the kind doesn't match, then fail + if (!current || current.kind !== parentKind) { + return undefined; + } + } + + // If we matched everything, then return the node that matched the last parentKinds item + return current as T; + } + + /** + * Does a depth-first search of the children of the specified node. Returns the first child + * with the specified kind, or undefined if there is no match. + */ + public static findFirstChildNode(node: ts.Node, kindToMatch: ts.SyntaxKind): T | undefined { + for (const child of node.getChildren()) { + if (child.kind === kindToMatch) { + return child as T; + } + + const recursiveMatch: T | undefined = TypeScriptHelpers.findFirstChildNode(child, kindToMatch); + if (recursiveMatch) { + return recursiveMatch; + } + } + + return undefined; + } + + /** + * Returns the first parent node with the specified SyntaxKind, or undefined if there is no match. + */ + public static findFirstParent(node: ts.Node, kindToMatch: ts.SyntaxKind): T | undefined { + let current: ts.Node | undefined = node.parent; + + while (current) { + if (current.kind === kindToMatch) { + return current as T; + } + + current = current.parent; + } + + return undefined; + } + + /** + * Returns the highest parent node with the specified SyntaxKind, or undefined if there is no match. + * + * @remarks + * Whereas findFirstParent() returns the first match, findHighestParent() returns the last match. + */ + public static findHighestParent(node: ts.Node, kindToMatch: ts.SyntaxKind): T | undefined { + let current: ts.Node | undefined = node; + let highest: T | undefined; + + for (;;) { + current = TypeScriptHelpers.findFirstParent(current, kindToMatch); + if (!current) { + break; + } + + highest = current as T; + } + + return highest; + } + + /** + * Decodes the names that the compiler generates for a built-in ECMAScript symbol. + * + * @remarks + * TypeScript binds well-known ECMAScript symbols like `[Symbol.iterator]` as `__@iterator`. + * If `name` is of this form, then `tryGetWellKnownSymbolName()` converts it back into e.g. `[Symbol.iterator]`. + * If the string does not start with `__@` then `undefined` is returned. + */ + public static tryDecodeWellKnownSymbolName(name: ts.__String): string | undefined { + const match = TypeScriptHelpers._wellKnownSymbolNameRegExp.exec(name as string); + if (match?.groups?.identifier) { + const identifier: string = match.groups.identifier; + return `[Symbol.${identifier}]`; + } + + return undefined; + } + + /** + * Returns whether the provided name was generated for a TypeScript `unique symbol`. + */ + public static isUniqueSymbolName(name: ts.__String): boolean { + return TypeScriptHelpers._uniqueSymbolNameRegExp.test(name as string); + } + + /** + * Derives the string representation of a TypeScript late-bound symbol. + */ + public static tryGetLateBoundName(declarationName: ts.ComputedPropertyName): string | undefined { + // Create a node printer that ignores comments and indentation that we can use to convert + // declarationName to a string. + const printer: ts.Printer = ts.createPrinter( + { removeComments: true }, + { + onEmitNode(hint: ts.EmitHint, node: ts.Node, emitCallback: (hint: ts.EmitHint, node: ts.Node) => void): void { + ts.setEmitFlags(declarationName, ts.EmitFlags.NoIndentation | ts.EmitFlags.SingleLine); + emitCallback(hint, node); + }, + }, + ); + const sourceFile: ts.SourceFile = declarationName.getSourceFile(); + const text: string = printer.printNode(ts.EmitHint.Unspecified, declarationName, sourceFile); + // clean up any emit flags we've set on any nodes in the tree. + ts.disposeEmitNodes(sourceFile); + return text; + } +} diff --git a/packages/api-extractor/src/analyzer/TypeScriptInternals.ts b/packages/api-extractor/src/analyzer/TypeScriptInternals.ts new file mode 100644 index 000000000..1a0176d58 --- /dev/null +++ b/packages/api-extractor/src/analyzer/TypeScriptInternals.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; + +/** + * Exposes the TypeScript compiler internals for detecting global variable names. + */ +export interface IGlobalVariableAnalyzer { + hasGlobalName(name: string): boolean; +} + +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 + * certain data structures, and seems to always work, but is not officially documented. + * + * @returns The associated Symbol. If there is no semantic information (e.g. if the + * declaration is an extra semicolon somewhere), then "undefined" is returned. + */ + public static tryGetSymbolForDeclaration( + declaration: ts.Declaration, + checker: ts.TypeChecker, + ): ts.Symbol | undefined { + let symbol: ts.Symbol | undefined = (declaration as any).symbol; + if (symbol && symbol.escapedName === ts.InternalSymbolName.Computed) { + const name: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration); + symbol = (name && checker.getSymbolAtLocation(name)) || symbol; + } + + return symbol; + } + + /** + * Returns whether the provided Symbol is a TypeScript "late-bound" Symbol (i.e. was created by the Checker + * for a computed property based on its type, rather than by the Binder). + */ + public static isLateBoundSymbol(symbol: ts.Symbol): boolean { + return ( + (symbol.flags & ts.SymbolFlags.Transient) !== 0 && + (ts as any).getCheckFlags(symbol) === (ts as any).CheckFlags.Late + ); + } + + /** + * Retrieves the comment ranges associated with the specified node. + */ + public static getJSDocCommentRanges(node: ts.Node, text: string): ts.CommentRange[] | undefined { + // Compiler internal: + // https://github.com/microsoft/TypeScript/blob/v2.4.2/src/compiler/utilities.ts#L616 + + return Reflect.apply((ts as any).getJSDocCommentRanges, this, [node, text]); + } + + /** + * Retrieves the (unescaped) value of an string literal, numeric literal, or identifier. + */ + public static getTextOfIdentifierOrLiteral(node: ts.Identifier | ts.NumericLiteral | ts.StringLiteralLike): string { + // Compiler internal: + // https://github.com/microsoft/TypeScript/blob/v3.2.2/src/compiler/utilities.ts#L2721 + + return (ts as any).getTextOfIdentifierOrLiteral(node); + } + + /** + * Retrieves the (cached) module resolution information for a module name that was exported from a SourceFile. + * The compiler populates this cache as part of analyzing the source file. + */ + public static getResolvedModule( + sourceFile: ts.SourceFile, + moduleNameText: string, + mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined, + ): ts.ResolvedModuleFull | undefined { + // Compiler internal: + // https://github.com/microsoft/TypeScript/blob/v4.7.2/src/compiler/utilities.ts#L161 + + return (ts as any).getResolvedModule(sourceFile, moduleNameText, mode); + } + + /** + * 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. + */ + public static getSymbolParent(symbol: ts.Symbol): ts.Symbol | undefined { + return (symbol as any).parent; + } + + /** + * In an statement like `export default class X { }`, the `Symbol.name` will be `default` + * whereas the `localSymbol` is `X`. + */ + public static tryGetLocalSymbol(declaration: ts.Declaration): ts.Symbol | undefined { + return (declaration as any).localSymbol; + } + + public static getGlobalVariableAnalyzer(program: ts.Program): IGlobalVariableAnalyzer { + const anyProgram: any = program; + const typeCheckerInstance: any = anyProgram.getDiagnosticsProducingTypeChecker ?? anyProgram.getTypeChecker; + + if (!typeCheckerInstance) { + throw new InternalError('Missing Program.getDiagnosticsProducingTypeChecker or Program.getTypeChecker'); + } + + const typeChecker: any = typeCheckerInstance(); + if (!typeChecker.getEmitResolver) { + throw new InternalError('Missing TypeChecker.getEmitResolver'); + } + + const resolver: any = typeChecker.getEmitResolver(); + if (!resolver.hasGlobalName) { + throw new InternalError('Missing EmitResolver.hasGlobalName'); + } + + return resolver; + } + + /** + * Returns whether a variable is declared with the const keyword + */ + public static isVarConst(node: ts.VariableDeclaration | ts.VariableDeclarationList): boolean { + // Compiler internal: https://github.com/microsoft/TypeScript/blob/71286e3d49c10e0e99faac360a6bbd40f12db7b6/src/compiler/utilities.ts#L925 + return (ts as any).isVarConst(node); + } +} diff --git a/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts b/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts new file mode 100644 index 000000000..c3d856feb --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts @@ -0,0 +1,125 @@ +// 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), + ); + }); + }); + }); + }); +}); + +/* eslint-enable @typescript-eslint/typedef */ diff --git a/packages/api-extractor/src/analyzer/test/SyntaxHelpers.test.ts b/packages/api-extractor/src/analyzer/test/SyntaxHelpers.test.ts new file mode 100644 index 000000000..da141d996 --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/SyntaxHelpers.test.ts @@ -0,0 +1,61 @@ +// 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) => { + return { 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", + }, + ] + `); + }); +}); diff --git a/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json new file mode 100644 index 000000000..10d00f267 --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-default", + "version": "1.0.0" +} diff --git a/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json new file mode 100644 index 000000000..01ad5c466 --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-inferred-from-main", + "version": "1.0.0", + "main": "path/to/main.js" +} diff --git a/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json new file mode 100644 index 000000000..b8d8f7640 --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json @@ -0,0 +1,7 @@ +{ + "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" +} diff --git a/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json new file mode 100644 index 000000000..31f3ad24a --- /dev/null +++ b/packages/api-extractor/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json @@ -0,0 +1,6 @@ +{ + "name": "package-inferred-from-typings", + "version": "1.0.0", + "main": "path/to/main.js", + "typings": "path/to/typings.d.ts" +} diff --git a/packages/api-extractor/src/api/CompilerState.ts b/packages/api-extractor/src/api/CompilerState.ts new file mode 100644 index 000000000..1a8c948d4 --- /dev/null +++ b/packages/api-extractor/src/api/CompilerState.ts @@ -0,0 +1,201 @@ +/* eslint-disable no-case-declarations */ +// 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 { JsonFile } from '@rushstack/node-core-library'; +import colors from 'colors'; +import * as ts from 'typescript'; +import type { IExtractorInvokeOptions } from './Extractor.js'; +import { ExtractorConfig } from './ExtractorConfig.js'; + +/** + * Options for {@link CompilerState.create} + * + * @public + */ +export interface ICompilerStateCreateOptions { + /** + * Additional .d.ts files to include in the analysis. + */ + additionalEntryPoints?: string[]; + + /** + * {@inheritDoc IExtractorInvokeOptions.typescriptCompilerFolder} + */ + typescriptCompilerFolder?: string | undefined; +} + +/** + * This class represents the TypeScript compiler state. This allows an optimization where multiple invocations + * of API Extractor can reuse the same TypeScript compiler analysis. + * + * @public + */ +export class CompilerState { + /** + * The TypeScript compiler's `Program` object, which represents a complete scope of analysis. + */ + public readonly program: unknown; + + private constructor(properties: CompilerState) { + this.program = properties.program; + } + + /** + * Create a compiler state for use with the specified `IExtractorInvokeOptions`. + */ + public static create(extractorConfig: ExtractorConfig, options?: ICompilerStateCreateOptions): CompilerState { + let tsconfig: {} | undefined = extractorConfig.overrideTsconfig; + let configBasePath: string = extractorConfig.projectFolder; + if (!tsconfig) { + // If it wasn't overridden, then load it from disk + tsconfig = JsonFile.load(extractorConfig.tsconfigFilePath); + configBasePath = path.resolve(path.dirname(extractorConfig.tsconfigFilePath)); + } + + const commandLine: ts.ParsedCommandLine = ts.parseJsonConfigFileContent(tsconfig, ts.sys, configBasePath); + + if (!commandLine.options.skipLibCheck && extractorConfig.skipLibCheck) { + commandLine.options.skipLibCheck = true; + console.log( + colors.cyan( + 'API Extractor was invoked with skipLibCheck. This is not recommended and may cause ' + + 'incorrect type analysis.', + ), + ); + } + + const inputFilePaths: string[] = commandLine.fileNames.concat(extractorConfig.mainEntryPointFilePath); + if (options?.additionalEntryPoints) { + inputFilePaths.push(...options.additionalEntryPoints); + } + + // Append the entry points and remove any non-declaration files from the list + const analysisFilePaths: string[] = CompilerState._generateFilePathsForAnalysis(inputFilePaths); + + const compilerHost: ts.CompilerHost = CompilerState._createCompilerHost(commandLine, options); + + const program: ts.Program = ts.createProgram(analysisFilePaths, commandLine.options, compilerHost); + + if (commandLine.errors.length > 0) { + const errorText: string = ts.flattenDiagnosticMessageText(commandLine.errors[0]!.messageText, '\n'); + throw new Error(`Error parsing tsconfig.json content: ${errorText}`); + } + + return new CompilerState({ + program, + }); + } + + /** + * Given a list of absolute file paths, return a list containing only the declaration + * files. Duplicates are also eliminated. + * + * @remarks + * The tsconfig.json settings specify the compiler's input (a set of *.ts source files, + * plus some *.d.ts declaration files used for legacy typings). However API Extractor + * analyzes the compiler's output (a set of *.d.ts entry point files, plus any legacy + * typings). This requires API Extractor to generate a special file list when it invokes + * the compiler. + * + * Duplicates are removed so that entry points can be appended without worrying whether they + * may already appear in the tsconfig.json file list. + */ + private static _generateFilePathsForAnalysis(inputFilePaths: string[]): string[] { + const analysisFilePaths: string[] = []; + + const seenFiles: Set = new Set(); + + for (const inputFilePath of inputFilePaths) { + const inputFileToUpper: string = inputFilePath.toUpperCase(); + if (!seenFiles.has(inputFileToUpper)) { + seenFiles.add(inputFileToUpper); + + if (!path.isAbsolute(inputFilePath)) { + throw new Error('Input file is not an absolute path: ' + inputFilePath); + } + + if (ExtractorConfig.hasDtsFileExtension(inputFilePath)) { + analysisFilePaths.push(inputFilePath); + } + } + } + + return analysisFilePaths; + } + + private static _createCompilerHost( + commandLine: ts.ParsedCommandLine, + options: IExtractorInvokeOptions | undefined, + ): ts.CompilerHost { + // Create a default CompilerHost that we will override + const compilerHost: ts.CompilerHost = ts.createCompilerHost(commandLine.options); + + // Save a copy of the original members. Note that "compilerHost" cannot be the copy, because + // createCompilerHost() captures that instance in a closure that is used by the members. + const defaultCompilerHost: ts.CompilerHost = { ...compilerHost }; + + if (options?.typescriptCompilerFolder) { + // Prevent a closure parameter + const typescriptCompilerLibFolder: string = path.join(options.typescriptCompilerFolder, 'lib'); + compilerHost.getDefaultLibLocation = () => typescriptCompilerLibFolder; + } + + // Used by compilerHost.fileExists() + // .d.ts file path --> whether the file exists + const dtsExistsCache: Map = new Map(); + + // Used by compilerHost.fileExists() + // Example: "c:/folder/file.part.ts" + const fileExtensionRegExp = /^(?.+)(?\.\w+)$/i; + + compilerHost.fileExists = (fileName: string): boolean => { + // In certain deprecated setups, the compiler may write its output files (.js and .d.ts) + // in the same folder as the corresponding input file (.ts or .tsx). When following imports, + // API Extractor wants to analyze the .d.ts file; however recent versions of the compiler engine + // will instead choose the .ts file. To work around this, we hook fileExists() to hide the + // existence of those files. + + // Is "fileName" a .d.ts file? The double extension ".d.ts" needs to be matched specially. + if (!ExtractorConfig.hasDtsFileExtension(fileName)) { + // It's not a .d.ts file. Is the file extension a potential source file? + const match: RegExpExecArray | null = fileExtensionRegExp.exec(fileName); + if (match?.groups?.pathWithoutExtension && match.groups?.fileExtension) { + // Example: "c:/folder/file.part" + const pathWithoutExtension: string = match.groups.pathWithoutExtension; + // Example: ".ts" + const fileExtension: string = match.groups.fileExtension; + + switch (fileExtension.toLocaleLowerCase()) { + case '.ts': + case '.tsx': + case '.js': + case '.jsx': + // Yes, this is a possible source file. Is there a corresponding .d.ts file in the same folder? + const dtsFileName = `${pathWithoutExtension}.d.ts`; + + let dtsFileExists: boolean | undefined = dtsExistsCache.get(dtsFileName); + if (dtsFileExists === undefined) { + dtsFileExists = defaultCompilerHost.fileExists!(dtsFileName); + dtsExistsCache.set(dtsFileName, dtsFileExists); + } + + if (dtsFileExists) { + // fileName is a potential source file and a corresponding .d.ts file exists. + // Thus, API Extractor should ignore this file (so the .d.ts file will get analyzed instead). + return false; + } + + break; + } + } + } + + // Fall through to the default implementation + return defaultCompilerHost.fileExists!(fileName); + }; + + return compilerHost; + } +} diff --git a/packages/api-extractor/src/api/ConsoleMessageId.ts b/packages/api-extractor/src/api/ConsoleMessageId.ts new file mode 100644 index 000000000..8f9884c20 --- /dev/null +++ b/packages/api-extractor/src/api/ConsoleMessageId.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Unique identifiers for console messages reported by API Extractor. + * + * @remarks + * + * These strings are possible values for the {@link ExtractorMessage.messageId} property + * when the `ExtractorMessage.category` is {@link ExtractorMessageCategory.Console}. + * @public + */ +export const enum ConsoleMessageId { + /** + * "You have changed the public API signature for this project. Updating ___" + */ + ApiReportCopied = 'console-api-report-copied', + + /** + * "The API report file was missing, so a new file was created. Please add this file to Git: ___" + */ + ApiReportCreated = 'console-api-report-created', + + /** + * "Unable to create the API report file. Please make sure the target folder exists: ___" + */ + ApiReportFolderMissing = 'console-api-report-folder-missing', + + /** + * "You have changed the public API signature for this project. + * Please copy the file ___ to ___, or perform a local build (which does this automatically). + * See the Git repo documentation for more info." + * + * OR + * + * "The API report file is missing. + * Please copy the file ___ to ___, or perform a local build (which does this automatically). + * See the Git repo documentation for more info." + */ + ApiReportNotCopied = 'console-api-report-not-copied', + + /** + * "The API report is up to date: ___" + */ + ApiReportUnchanged = 'console-api-report-unchanged', + + /** + * "The target project appears to use TypeScript ___ which is newer than the bundled compiler engine; + * consider upgrading API Extractor." + */ + CompilerVersionNotice = 'console-compiler-version-notice', + + /** + * Used for the information printed when the "--diagnostics" flag is enabled. + */ + Diagnostics = 'console-diagnostics', + + /** + * "Found metadata in ___" + */ + FoundTSDocMetadata = 'console-found-tsdoc-metadata', + + /** + * "Analysis will use the bundled TypeScript version ___" + */ + Preamble = 'console-preamble', + + /** + * "Using custom TSDoc config from ___" + */ + UsingCustomTSDocConfig = 'console-using-custom-tsdoc-config', + + /** + * "Writing: ___" + */ + WritingDocModelFile = 'console-writing-doc-model-file', + + /** + * "Writing package typings: ___" + */ + WritingDtsRollup = 'console-writing-dts-rollup', +} diff --git a/packages/api-extractor/src/api/Extractor.ts b/packages/api-extractor/src/api/Extractor.ts new file mode 100644 index 000000000..c4258abea --- /dev/null +++ b/packages/api-extractor/src/api/Extractor.ts @@ -0,0 +1,467 @@ +// 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 type { ApiPackage } from '@discordjs/api-extractor-model'; +import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import { + FileSystem, + type NewlineKind, + PackageJsonLookup, + type IPackageJson, + type INodePackageJson, + Path, +} from '@rushstack/node-core-library'; +import * as resolve from 'resolve'; +import * as semver from 'semver'; +import * as ts from 'typescript'; +import { PackageMetadataManager } from '../analyzer/PackageMetadataManager.js'; +import { Collector } from '../collector/Collector.js'; +import { MessageRouter } from '../collector/MessageRouter.js'; +import { SourceMapper } from '../collector/SourceMapper.js'; +import { DocCommentEnhancer } from '../enhancers/DocCommentEnhancer.js'; +import { ValidationEnhancer } from '../enhancers/ValidationEnhancer.js'; +import { ApiModelGenerator } from '../generators/ApiModelGenerator.js'; +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 type { ExtractorMessage } from './ExtractorMessage.js'; + +/** + * Runtime options for Extractor. + * + * @public + */ +export interface IExtractorInvokeOptions { + /** + * An optional TypeScript compiler state. This allows an optimization where multiple invocations of API Extractor + * can reuse the same TypeScript compiler analysis. + */ + compilerState?: CompilerState; + + /** + * Indicates that API Extractor is running as part of a local build, e.g. on developer's + * machine. + * + * @remarks + * This disables certain validation that would normally be performed for a ship/production build. For example, + * the *.api.md report file is automatically updated in a local build. + * + * The default value is false. + */ + localBuild?: boolean; + + /** + * An optional callback function that will be called for each `ExtractorMessage` before it is displayed by + * API Extractor. The callback can customize the message, handle it, or discard it. + * + * @remarks + * If a `messageCallback` is not provided, then by default API Extractor will print the messages to + * the STDERR/STDOUT console. + */ + messageCallback?(this: void, message: ExtractorMessage): void; + + /** + * If true, API Extractor will print diagnostic information used for troubleshooting problems. + * These messages will be included as {@link ExtractorLogLevel.Verbose} output. + * + * @remarks + * Setting `showDiagnostics=true` forces `showVerboseMessages=true`. + */ + showDiagnostics?: boolean; + + /** + * If true, API Extractor will include {@link ExtractorLogLevel.Verbose} messages in its output. + */ + showVerboseMessages?: boolean; + + /** + * Specifies an alternate folder path to be used when loading the TypeScript system typings. + * + * @remarks + * API Extractor uses its own TypeScript compiler engine to analyze your project. If your project + * is built with a significantly different TypeScript version, sometimes API Extractor may report compilation + * errors due to differences in the system typings (e.g. lib.dom.d.ts). You can use the "--typescriptCompilerFolder" + * option to specify the folder path where you installed the TypeScript package, and API Extractor's compiler will + * use those system typings instead. + */ + typescriptCompilerFolder?: string | undefined; +} + +/** + * This object represents the outcome of an invocation of API Extractor. + * + * @public + */ +export class ExtractorResult { + /** + * The TypeScript compiler state that was used. + */ + public readonly compilerState: CompilerState; + + /** + * The API Extractor configuration that was used. + */ + public readonly extractorConfig: ExtractorConfig; + + /** + * Whether the invocation of API Extractor was successful. For example, if `succeeded` is false, then the build task + * would normally return a nonzero process exit code, indicating that the operation failed. + * + * @remarks + * + * Normally the operation "succeeds" if `errorCount` and `warningCount` are both zero. However if + * {@link IExtractorInvokeOptions.localBuild} is `true`, then the operation "succeeds" if `errorCount` is zero + * (i.e. warnings are ignored). + */ + public readonly succeeded: boolean; + + /** + * Returns true if the API report was found to have changed. + */ + public readonly apiReportChanged: boolean; + + /** + * Reports the number of errors encountered during analysis. + * + * @remarks + * This does not count exceptions, where unexpected issues prematurely abort the operation. + */ + public readonly errorCount: number; + + /** + * Reports the number of warnings encountered during analysis. + * + * @remarks + * This does not count warnings that are emitted in the API report file. + */ + public readonly warningCount: number; + + /** + * @internal + */ + public constructor(properties: ExtractorResult) { + this.compilerState = properties.compilerState; + this.extractorConfig = properties.extractorConfig; + this.succeeded = properties.succeeded; + this.apiReportChanged = properties.apiReportChanged; + this.errorCount = properties.errorCount; + this.warningCount = properties.warningCount; + } +} + +/** + * The starting point for invoking the API Extractor tool. + * + * @public + */ +export class Extractor { + /** + * Returns the version number of the API Extractor NPM package. + */ + public static get version(): string { + return Extractor._getPackageJson().version; + } + + /** + * Returns the package name of the API Extractor NPM package. + */ + public static get packageName(): string { + return Extractor._getPackageJson().name; + } + + private static _getPackageJson(): IPackageJson { + return PackageJsonLookup.loadOwnPackageJson(__dirname); + } + + /** + * Load the api-extractor.json config file from the specified path, and then invoke API Extractor. + */ + public static loadConfigAndInvoke(configFilePath: string, options?: IExtractorInvokeOptions): ExtractorResult { + const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(configFilePath); + + return Extractor.invoke(extractorConfig, options); + } + + /** + * Invoke API Extractor using an already prepared `ExtractorConfig` object. + */ + public static invoke(extractorConfig: ExtractorConfig, options?: IExtractorInvokeOptions): ExtractorResult { + const ioptions = options ?? {}; + + const localBuild: boolean = ioptions.localBuild ?? false; + + let compilerState: CompilerState | undefined; + if (ioptions.compilerState) { + compilerState = ioptions.compilerState; + } else { + compilerState = CompilerState.create(extractorConfig, ioptions); + } + + const sourceMapper: SourceMapper = new SourceMapper(); + + const messageRouter: MessageRouter = new MessageRouter({ + workingPackageFolder: extractorConfig.packageFolder, + messageCallback: ioptions.messageCallback, + messagesConfig: extractorConfig.messages || {}, + showVerboseMessages: Boolean(ioptions.showVerboseMessages), + showDiagnostics: Boolean(ioptions.showDiagnostics), + tsdocConfiguration: extractorConfig.tsdocConfiguration, + sourceMapper, + }); + + if ( + extractorConfig.tsdocConfigFile.filePath && + !extractorConfig.tsdocConfigFile.fileNotFound && + !Path.isEqual(extractorConfig.tsdocConfigFile.filePath, ExtractorConfig._tsdocBaseFilePath) + ) { + messageRouter.logVerbose( + ConsoleMessageId.UsingCustomTSDocConfig, + 'Using custom TSDoc config from ' + extractorConfig.tsdocConfigFile.filePath, + ); + } + + this._checkCompilerCompatibility(extractorConfig, messageRouter); + + if (messageRouter.showDiagnostics) { + messageRouter.logDiagnostic(''); + messageRouter.logDiagnosticHeader('Final prepared ExtractorConfig'); + messageRouter.logDiagnostic(extractorConfig.getDiagnosticDump()); + messageRouter.logDiagnosticFooter(); + + messageRouter.logDiagnosticHeader('Compiler options'); + const serializedCompilerOptions: object = MessageRouter.buildJsonDumpObject( + (compilerState.program as ts.Program).getCompilerOptions(), + ); + messageRouter.logDiagnostic(JSON.stringify(serializedCompilerOptions, undefined, 2)); + messageRouter.logDiagnosticFooter(); + + messageRouter.logDiagnosticHeader('TSDoc configuration'); + // Convert the TSDocConfiguration into a tsdoc.json representation + const combinedConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(extractorConfig.tsdocConfiguration); + const serializedTSDocConfig: object = MessageRouter.buildJsonDumpObject(combinedConfigFile.saveToObject()); + messageRouter.logDiagnostic(JSON.stringify(serializedTSDocConfig, undefined, 2)); + messageRouter.logDiagnosticFooter(); + } + + const collector: Collector = new Collector({ + program: compilerState.program as ts.Program, + messageRouter, + extractorConfig, + sourceMapper, + }); + + collector.analyze(); + + DocCommentEnhancer.analyze(collector); + ValidationEnhancer.analyze(collector); + + const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector); + const apiPackage: ApiPackage = modelBuilder.buildApiPackage(); + + if (messageRouter.showDiagnostics) { + messageRouter.logDiagnostic(''); // skip a line after any diagnostic messages + } + + if (extractorConfig.docModelEnabled) { + messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, 'Writing: ' + extractorConfig.apiJsonFilePath); + apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, { + toolPackage: Extractor.packageName, + toolVersion: Extractor.version, + + newlineConversion: extractorConfig.newlineKind, + ensureFolderExists: true, + testMode: extractorConfig.testMode, + }); + } + + let apiReportChanged = 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.`, + ); + } + } + } + + if (extractorConfig.rollupEnabled) { + Extractor._generateRollupDtsFile( + collector, + extractorConfig.publicTrimmedFilePath, + DtsRollupKind.PublicRelease, + extractorConfig.newlineKind, + ); + Extractor._generateRollupDtsFile( + collector, + extractorConfig.alphaTrimmedFilePath, + DtsRollupKind.AlphaRelease, + extractorConfig.newlineKind, + ); + Extractor._generateRollupDtsFile( + collector, + extractorConfig.betaTrimmedFilePath, + DtsRollupKind.BetaRelease, + extractorConfig.newlineKind, + ); + Extractor._generateRollupDtsFile( + collector, + extractorConfig.untrimmedFilePath, + DtsRollupKind.InternalRelease, + extractorConfig.newlineKind, + ); + } + + if (extractorConfig.tsdocMetadataEnabled) { + // Write the tsdoc-metadata.json file for this project + PackageMetadataManager.writeTsdocMetadataFile(extractorConfig.tsdocMetadataFilePath, extractorConfig.newlineKind); + } + + // Show all the messages that we collected during analysis + messageRouter.handleRemainingNonConsoleMessages(); + + // Determine success + let succeeded: boolean; + if (localBuild) { + // For a local build, fail if there were errors (but ignore warnings) + succeeded = messageRouter.errorCount === 0; + } else { + // For a production build, fail if there were any errors or warnings + succeeded = messageRouter.errorCount + messageRouter.warningCount === 0; + } + + return new ExtractorResult({ + compilerState, + extractorConfig, + succeeded, + apiReportChanged, + errorCount: messageRouter.errorCount, + warningCount: messageRouter.warningCount, + }); + } + + private static _checkCompilerCompatibility(extractorConfig: ExtractorConfig, messageRouter: MessageRouter): void { + messageRouter.logInfo(ConsoleMessageId.Preamble, `Analysis will use the bundled TypeScript version ${ts.version}`); + + try { + const typescriptPath: string = resolve.sync('typescript', { + basedir: extractorConfig.projectFolder, + preserveSymlinks: false, + }); + const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); + const packageJson: INodePackageJson | undefined = packageJsonLookup.tryLoadNodePackageJsonFor(typescriptPath); + if (packageJson?.version && semver.valid(packageJson.version)) { + // Consider a newer MINOR release to be incompatible + const ourMajor: number = semver.major(ts.version); + const ourMinor: number = semver.minor(ts.version); + + const theirMajor: number = semver.major(packageJson.version); + const theirMinor: number = semver.minor(packageJson.version); + + if (theirMajor > ourMajor || (theirMajor === ourMajor && theirMinor > ourMinor)) { + messageRouter.logInfo( + ConsoleMessageId.CompilerVersionNotice, + `*** The target project appears to use TypeScript ${packageJson.version} which is newer than the` + + ` bundled compiler engine; consider upgrading API Extractor.`, + ); + } + } + } catch { + // The compiler detection heuristic is not expected to work in many configurations + } + } + + private static _generateRollupDtsFile( + collector: Collector, + outputPath: string, + dtsKind: DtsRollupKind, + newlineKind: NewlineKind, + ): void { + if (outputPath !== '') { + collector.messageRouter.logVerbose(ConsoleMessageId.WritingDtsRollup, `Writing package typings: ${outputPath}`); + DtsRollupGenerator.writeTypingsFile(collector, outputPath, dtsKind, newlineKind); + } + } +} diff --git a/packages/api-extractor/src/api/ExtractorConfig.ts b/packages/api-extractor/src/api/ExtractorConfig.ts new file mode 100644 index 000000000..3a058cd96 --- /dev/null +++ b/packages/api-extractor/src/api/ExtractorConfig.ts @@ -0,0 +1,1196 @@ +// 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 { EnumMemberOrder } from '@discordjs/api-extractor-model'; +import { TSDocConfiguration } from '@microsoft/tsdoc'; +import { TSDocConfigFile } from '@microsoft/tsdoc-config'; +import { + JsonFile, + JsonSchema, + FileSystem, + PackageJsonLookup, + type INodePackageJson, + PackageName, + Text, + InternalError, + Path, + NewlineKind, +} from '@rushstack/node-core-library'; +import { type IRigConfig, RigConfig } from '@rushstack/rig-package'; +import cloneDeep from 'lodash/cloneDeep.js'; +import merge from 'lodash/merge.js'; +import * as resolve from 'resolve'; +import { PackageMetadataManager } from '../analyzer/PackageMetadataManager.js'; +import { MessageRouter } from '../collector/MessageRouter.js'; +import apiExtractorSchema from '../schemas/api-extractor.schema.json' assert { type: 'json' }; +import type { IConfigFile, IExtractorMessagesConfig } from './IConfigFile.js'; + +/** + * Tokens used during variable expansion of path fields from api-extractor.json. + */ +interface IExtractorConfigTokenContext { + /** + * The `` token returns the project's full NPM package name including any NPM scope. + * If there is no associated package.json file, then the value is `unknown-package`. + * + * Example: `@scope/my-project` + */ + packageName: string; + + /** + * The `` token returns the expanded `"projectFolder"` setting from api-extractor.json. + */ + projectFolder: string; + + /** + * The `` token returns the project's NPM package name, without any NPM scope. + * If there is no associated package.json file, then the value is `unknown-package`. + * + * Example: `my-project` + */ + unscopedPackageName: string; +} + +/** + * Options for {@link ExtractorConfig.tryLoadForFolder}. + * + * @public + */ +export interface IExtractorConfigLoadForFolderOptions { + /** + * An already constructed `PackageJsonLookup` cache object to use. If omitted, a temporary one will + * be constructed. + */ + packageJsonLookup?: PackageJsonLookup; + + /** + * An already constructed `RigConfig` object. If omitted, then a new `RigConfig` object will be constructed. + */ + rigConfig?: IRigConfig; + + /** + * The folder path to start from when searching for api-extractor.json. + */ + startingFolder: string; +} + +/** + * Options for {@link ExtractorConfig.prepare}. + * + * @public + */ +export interface IExtractorConfigPrepareOptions { + /** + * A configuration object as returned by {@link ExtractorConfig.loadFile}. + */ + configObject: IConfigFile; + + /** + * The absolute path of the file that the `configObject` object was loaded from. This is used for error messages + * and when probing for `tsconfig.json`. + * + * @remarks + * + * If `configObjectFullPath` and `projectFolderLookupToken` are both unspecified, then the api-extractor.json + * config file must explicitly specify a `projectFolder` setting rather than relying on the `` token. + */ + configObjectFullPath: string | undefined; + + /** + * When preparing the configuration object, folder and file paths referenced in the configuration are checked + * for existence, and an error is reported if they are not found. This option can be used to disable this + * check for the main entry point module. This may be useful when preparing a configuration file for an + * un-built project. + */ + ignoreMissingEntryPoint?: boolean; + + /** + * The parsed package.json file for the working package, or undefined if API Extractor was invoked without + * a package.json file. + * + * @remarks + * + * If omitted, then the `` and `` tokens will have default values. + */ + packageJson?: INodePackageJson | undefined; + + /** + * The absolute path of the file that the `packageJson` object was loaded from, or undefined if API Extractor + * was invoked without a package.json file. + * + * @remarks + * + * This is used for error messages and when resolving paths found in package.json. + * + * If `packageJsonFullPath` is specified but `packageJson` is omitted, the file will be loaded automatically. + */ + packageJsonFullPath: string | undefined; + + /** + * The default value for the `projectFolder` setting is the `` token, which uses a heuristic to guess + * an appropriate project folder. Use `projectFolderLookupValue` to manually specify the `` token value + * instead. + * + * @remarks + * If the `projectFolder` setting is explicitly specified in api-extractor.json file, it should take precedence + * over a value specified via the API. Thus the `projectFolderLookupToken` option provides a way to override + * the default value for `projectFolder` setting while still honoring a manually specified value. + */ + projectFolderLookupToken?: string | undefined; + + /** + * Allow customization of the tsdoc.json config file. If omitted, this file will be loaded from its default + * location. If the file does not exist, then the standard definitions will be used from + * `@microsoft/api-extractor/extends/tsdoc-base.json`. + */ + tsdocConfigFile?: TSDocConfigFile; +} + +interface IExtractorConfigParameters { + alphaTrimmedFilePath: string; + apiJsonFilePath: string; + apiReportEnabled: boolean; + apiReportIncludeForgottenExports: boolean; + betaTrimmedFilePath: string; + bundledPackages: string[]; + docModelEnabled: boolean; + docModelIncludeForgottenExports: boolean; + enumMemberOrder: EnumMemberOrder; + mainEntryPointFilePath: string; + messages: IExtractorMessagesConfig; + newlineKind: NewlineKind; + omitTrimmingComments: boolean; + overrideTsconfig: {} | undefined; + packageFolder: string | undefined; + packageJson: INodePackageJson | undefined; + projectFolder: string; + projectFolderUrl: string | undefined; + publicTrimmedFilePath: string; + reportFilePath: string; + reportTempFilePath: string; + rollupEnabled: boolean; + skipLibCheck: boolean; + testMode: boolean; + tsconfigFilePath: string; + tsdocConfigFile: TSDocConfigFile; + tsdocConfiguration: TSDocConfiguration; + tsdocMetadataEnabled: boolean; + tsdocMetadataFilePath: string; + untrimmedFilePath: string; +} + +/** + * The `ExtractorConfig` class loads, validates, interprets, and represents the api-extractor.json config file. + * + * @public + */ +export class ExtractorConfig { + /** + * The JSON Schema for API Extractor config file (api-extractor.schema.json). + */ + public static readonly jsonSchema: JsonSchema = JsonSchema.fromLoadedObject(apiExtractorSchema); + + /** + * The config file name "api-extractor.json". + */ + public static readonly FILENAME = 'api-extractor.json' as const; + + /** + * The full path to `extends/tsdoc-base.json` which contains the standard TSDoc configuration + * for API Extractor. + * + * @internal + */ + public static readonly _tsdocBaseFilePath: string = path.resolve(__dirname, './extends/tsdoc-base.json'); + + private static readonly _defaultConfig: Partial = JsonFile.load( + path.join(__dirname, './schemas/api-extractor-defaults.json'), + ); + + private static readonly _declarationFileExtensionRegExp: RegExp = /\.d\.[cm]?ts$/i; + + /** + * {@inheritDoc IConfigFile.projectFolder} + */ + public readonly projectFolder: string; + + /** + * The parsed package.json file for the working package, or undefined if API Extractor was invoked without + * a package.json file. + */ + public readonly packageJson: INodePackageJson | undefined; + + /** + * The absolute path of the folder containing the package.json file for the working package, or undefined + * if API Extractor was invoked without a package.json file. + */ + public readonly packageFolder: string | undefined; + + /** + * {@inheritDoc IConfigFile.mainEntryPointFilePath} + */ + public readonly mainEntryPointFilePath: string; + + /** + * {@inheritDoc IConfigFile.bundledPackages} + */ + public readonly bundledPackages: string[]; + + /** + * {@inheritDoc IConfigCompiler.tsconfigFilePath} + */ + public readonly tsconfigFilePath: string; + + /** + * {@inheritDoc IConfigCompiler.overrideTsconfig} + */ + public readonly overrideTsconfig: {} | undefined; + + /** + * {@inheritDoc IConfigCompiler.skipLibCheck} + */ + public readonly skipLibCheck: boolean; + + /** + * {@inheritDoc IConfigApiReport.enabled} + */ + public readonly apiReportEnabled: boolean; + + /** + * The `reportFolder` path combined with the `reportFileName`. + */ + public readonly reportFilePath: string; + + /** + * The `reportTempFolder` path combined with the `reportFileName`. + */ + public readonly reportTempFilePath: string; + + /** + * {@inheritDoc IConfigApiReport.includeForgottenExports} + */ + public readonly apiReportIncludeForgottenExports: boolean; + + /** + * {@inheritDoc IConfigDocModel.enabled} + */ + public readonly docModelEnabled: boolean; + + /** + * {@inheritDoc IConfigDocModel.apiJsonFilePath} + */ + public readonly apiJsonFilePath: string; + + /** + * {@inheritDoc IConfigDocModel.includeForgottenExports} + */ + public readonly docModelIncludeForgottenExports: boolean; + + /** + * {@inheritDoc IConfigDocModel.projectFolderUrl} + */ + public readonly projectFolderUrl: string | undefined; + + /** + * {@inheritDoc IConfigDtsRollup.enabled} + */ + public readonly rollupEnabled: boolean; + + /** + * {@inheritDoc IConfigDtsRollup.untrimmedFilePath} + */ + public readonly untrimmedFilePath: string; + + /** + * {@inheritDoc IConfigDtsRollup.alphaTrimmedFilePath} + */ + public readonly alphaTrimmedFilePath: string; + + /** + * {@inheritDoc IConfigDtsRollup.betaTrimmedFilePath} + */ + public readonly betaTrimmedFilePath: string; + + /** + * {@inheritDoc IConfigDtsRollup.publicTrimmedFilePath} + */ + public readonly publicTrimmedFilePath: string; + + /** + * {@inheritDoc IConfigDtsRollup.omitTrimmingComments} + */ + public readonly omitTrimmingComments: boolean; + + /** + * {@inheritDoc IConfigTsdocMetadata.enabled} + */ + public readonly tsdocMetadataEnabled: boolean; + + /** + * {@inheritDoc IConfigTsdocMetadata.tsdocMetadataFilePath} + */ + public readonly tsdocMetadataFilePath: string; + + /** + * The tsdoc.json configuration that will be used when parsing doc comments. + */ + public readonly tsdocConfigFile: TSDocConfigFile; + + /** + * The `TSDocConfiguration` loaded from {@link ExtractorConfig.tsdocConfigFile}. + */ + public readonly tsdocConfiguration: TSDocConfiguration; + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. + */ + public readonly newlineKind: NewlineKind; + + /** + * {@inheritDoc IConfigFile.messages} + */ + public readonly messages: IExtractorMessagesConfig; + + /** + * {@inheritDoc IConfigFile.testMode} + */ + public readonly testMode: boolean; + + /** + * {@inheritDoc IConfigFile.enumMemberOrder} + */ + 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; + } + + /** + * Returns a JSON-like string representing the `ExtractorConfig` state, which can be printed to a console + * for diagnostic purposes. + * + * @remarks + * This is used by the "--diagnostics" command-line option. The string is not intended to be deserialized; + * its format may be changed at any time. + */ + public getDiagnosticDump(): string { + // Handle the simple JSON-serializable properties using buildJsonDumpObject() + const result: object = MessageRouter.buildJsonDumpObject(this, { + keyNamesToOmit: ['tsdocConfigFile', 'tsdocConfiguration'], + }); + + // Implement custom formatting for tsdocConfigFile and tsdocConfiguration + + (result as any).tsdocConfigFile = { + filePath: this.tsdocConfigFile.filePath, + log: this.tsdocConfigFile.log.messages.map((x) => x.toString()), + }; + + return JSON.stringify(result, undefined, 2); + } + + /** + * Returns a simplified file path for use in error messages. + * + * @internal + */ + public _getShortFilePath(absolutePath: string): string { + if (!path.isAbsolute(absolutePath)) { + throw new InternalError('Expected absolute path: ' + absolutePath); + } + + if (Path.isUnderOrEqual(absolutePath, this.projectFolder)) { + return Path.convertToSlashes(path.relative(this.projectFolder, absolutePath)); + } + + return absolutePath; + } + + /** + * Searches for the api-extractor.json config file associated with the specified starting folder, + * and loads the file if found. This lookup supports + * {@link https://www.npmjs.com/package/@rushstack/rig-package | rig packages}. + * + * @remarks + * The search will first look for a package.json file in a parent folder of the starting folder; + * if found, that will be used as the base folder instead of the starting folder. If the config + * file is not found in `/api-extractor.json` or `/config/api-extractor.json`, + * then `/api-extractor.json or /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 + // "" 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 "" should be + // the "" that referenced the rig. + projectFolderLookupToken = packageFolder; + } + } + + if (!FileSystem.exists(configFilename)) { + // API Extractor does not seem to be configured for this folder + return undefined; + } + } + } + + const configObjectFullPath: string = path.resolve(configFilename); + const configObject: IConfigFile = ExtractorConfig.loadFile(configObjectFullPath); + + return { + configObject, + configObjectFullPath, + packageJsonFullPath, + projectFolderLookupToken, + }; + } + + /** + * Loads the api-extractor.json config file from the specified file path, and prepares an `ExtractorConfig` object. + * + * @remarks + * Loads the api-extractor.json config file from the specified file path. If the "extends" field is present, + * the referenced file(s) will be merged. For any omitted fields, the API Extractor default values are merged. + * + * The result is prepared using `ExtractorConfig.prepare()`. + */ + public static loadFileAndPrepare(configJsonFilePath: string): ExtractorConfig { + const configObjectFullPath: string = path.resolve(configJsonFilePath); + const configObject: IConfigFile = ExtractorConfig.loadFile(configObjectFullPath); + + const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); + const packageJsonFullPath: string | undefined = + packageJsonLookup.tryGetPackageJsonFilePathFor(configObjectFullPath); + + const extractorConfig: ExtractorConfig = ExtractorConfig.prepare({ + configObject, + configObjectFullPath, + packageJsonFullPath, + }); + + return extractorConfig; + } + + /** + * Performs only the first half of {@link ExtractorConfig.loadFileAndPrepare}, providing an opportunity to + * modify the object before it is passed to {@link ExtractorConfig.prepare}. + * + * @remarks + * Loads the api-extractor.json config file from the specified file path. If the "extends" field is present, + * the referenced file(s) will be merged. For any omitted fields, the API Extractor default values are merged. + */ + public static loadFile(jsonFilePath: string): IConfigFile { + // Set to keep track of config files which have been processed. + const visitedPaths: Set = new Set(); + + let currentConfigFilePath: string = path.resolve(jsonFilePath); + let configObject: Partial = {}; + + try { + do { + // Check if this file was already processed. + if (visitedPaths.has(currentConfigFilePath)) { + throw new Error( + `The API Extractor "extends" setting contains a cycle.` + + ` This file is included twice: "${currentConfigFilePath}"`, + ); + } + + visitedPaths.add(currentConfigFilePath); + + const currentConfigFolderPath: string = path.dirname(currentConfigFilePath); + + // Load the extractor config defined in extends property. + const baseConfig: IConfigFile = JsonFile.load(currentConfigFilePath); + + let extendsField: string = baseConfig.extends ?? ''; + + // Delete the "extends" field so it doesn't get merged + delete baseConfig.extends; + + if (extendsField) { + if (/^\.\.?[/\\]/.test(extendsField)) { + // EXAMPLE: "./subfolder/api-extractor-base.json" + extendsField = path.resolve(currentConfigFolderPath, extendsField); + } else { + // EXAMPLE: "my-package/api-extractor-base.json" + // + // Resolve "my-package" from the perspective of the current folder. + try { + extendsField = resolve.sync(extendsField, { + basedir: currentConfigFolderPath, + }); + } catch (error) { + throw new Error(`Error resolving NodeJS path "${extendsField}": ${(error as Error).message}`); + } + } + } + + // This step has to be performed in advance, since the currentConfigFolderPath information will be lost + // after lodash.merge() is performed. + ExtractorConfig._resolveConfigFileRelativePaths(baseConfig, currentConfigFolderPath); + + // Merge extractorConfig into baseConfig, mutating baseConfig + merge(baseConfig, configObject); + configObject = baseConfig; + + currentConfigFilePath = extendsField; + } while (currentConfigFilePath); + } catch (error) { + throw new Error(`Error loading ${currentConfigFilePath}:\n` + (error as Error).message); + } + + // Lastly, apply the defaults + configObject = merge(cloneDeep(ExtractorConfig._defaultConfig), configObject); + + ExtractorConfig.jsonSchema.validateObject(configObject, jsonFilePath); + + // The schema validation should ensure that this object conforms to IConfigFile + return configObject as IConfigFile; + } + + private static _resolveConfigFileRelativePaths(configFile: IConfigFile, currentConfigFolderPath: string): void { + if (configFile.projectFolder) { + configFile.projectFolder = ExtractorConfig._resolveConfigFileRelativePath( + 'projectFolder', + configFile.projectFolder, + currentConfigFolderPath, + ); + } + + if (configFile.mainEntryPointFilePath) { + configFile.mainEntryPointFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'mainEntryPointFilePath', + configFile.mainEntryPointFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.compiler?.tsconfigFilePath) { + configFile.compiler.tsconfigFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'tsconfigFilePath', + configFile.compiler.tsconfigFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.apiReport) { + if (configFile.apiReport.reportFolder) { + configFile.apiReport.reportFolder = ExtractorConfig._resolveConfigFileRelativePath( + 'reportFolder', + configFile.apiReport.reportFolder, + currentConfigFolderPath, + ); + } + + if (configFile.apiReport.reportTempFolder) { + configFile.apiReport.reportTempFolder = ExtractorConfig._resolveConfigFileRelativePath( + 'reportTempFolder', + configFile.apiReport.reportTempFolder, + currentConfigFolderPath, + ); + } + } + + if (configFile.docModel?.apiJsonFilePath) { + configFile.docModel.apiJsonFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'apiJsonFilePath', + configFile.docModel.apiJsonFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.dtsRollup) { + if (configFile.dtsRollup.untrimmedFilePath) { + configFile.dtsRollup.untrimmedFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'untrimmedFilePath', + configFile.dtsRollup.untrimmedFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.dtsRollup.alphaTrimmedFilePath) { + configFile.dtsRollup.alphaTrimmedFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'alphaTrimmedFilePath', + configFile.dtsRollup.alphaTrimmedFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.dtsRollup.betaTrimmedFilePath) { + configFile.dtsRollup.betaTrimmedFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'betaTrimmedFilePath', + configFile.dtsRollup.betaTrimmedFilePath, + currentConfigFolderPath, + ); + } + + if (configFile.dtsRollup.publicTrimmedFilePath) { + configFile.dtsRollup.publicTrimmedFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'publicTrimmedFilePath', + configFile.dtsRollup.publicTrimmedFilePath, + currentConfigFolderPath, + ); + } + } + + if (configFile.tsdocMetadata?.tsdocMetadataFilePath) { + configFile.tsdocMetadata.tsdocMetadataFilePath = ExtractorConfig._resolveConfigFileRelativePath( + 'tsdocMetadataFilePath', + configFile.tsdocMetadata.tsdocMetadataFilePath, + currentConfigFolderPath, + ); + } + } + + private static _resolveConfigFileRelativePath( + _fieldName: string, + fieldValue: string, + currentConfigFolderPath: string, + ): string { + if (!path.isAbsolute(fieldValue) && !fieldValue.startsWith('')) { + // If the path is not absolute and does not start with "", then resolve it relative + // to the folder of the config file that it appears in + return path.join(currentConfigFolderPath, fieldValue); + } + + return fieldValue; + } + + /** + * Prepares an `ExtractorConfig` object using a configuration that is provided as a runtime object, + * rather than reading it from disk. This allows configurations to be constructed programmatically, + * loaded from an alternate source, and/or customized after loading. + */ + public static prepare(options: IExtractorConfigPrepareOptions): ExtractorConfig { + const filenameForErrors: string = options.configObjectFullPath ?? 'the configuration object'; + const configObject: Partial = options.configObject; + + if (configObject.extends) { + throw new Error('The IConfigFile.extends field must be expanded before calling ExtractorConfig.prepare()'); + } + + if (options.configObjectFullPath && !path.isAbsolute(options.configObjectFullPath)) { + throw new Error('The "configObjectFullPath" setting must be an absolute path'); + } + + ExtractorConfig.jsonSchema.validateObject(configObject, filenameForErrors); + + const packageJsonFullPath: string | undefined = options.packageJsonFullPath; + let packageFolder: string | undefined; + let packageJson: INodePackageJson | undefined; + + if (packageJsonFullPath) { + if (!/.json$/i.test(packageJsonFullPath)) { + // Catch common mistakes e.g. where someone passes a folder path instead of a file path + throw new Error('The "packageJsonFullPath" setting does not have a .json file extension'); + } + + if (!path.isAbsolute(packageJsonFullPath)) { + throw new Error('The "packageJsonFullPath" setting must be an absolute path'); + } + + if (options.packageJson) { + packageJson = options.packageJson; + } else { + const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup(); + packageJson = packageJsonLookup.loadNodePackageJson(packageJsonFullPath); + } + + packageFolder = path.dirname(packageJsonFullPath); + } + + // "tsdocConfigFile" and "tsdocConfiguration" are prepared outside the try-catch block, + // so that if exceptions are thrown, it will not get the "Error parsing api-extractor.json:" header + let extractorConfigParameters: Omit; + + try { + if (!configObject.compiler) { + // A merged configuration should have this + throw new Error('The "compiler" section is missing'); + } + + if (!configObject.projectFolder) { + // A merged configuration should have this + throw new Error('The "projectFolder" setting is missing'); + } + + let projectFolder: string; + if (configObject.projectFolder.trim() === '') { + if (options.projectFolderLookupToken) { + // Use the manually specified "" value + projectFolder = options.projectFolderLookupToken; + + if (!FileSystem.exists(options.projectFolderLookupToken)) { + throw new Error( + 'The specified "projectFolderLookupToken" path does not exist: ' + options.projectFolderLookupToken, + ); + } + } else { + if (!options.configObjectFullPath) { + throw new Error( + 'The "projectFolder" setting uses the "" token, but it cannot be expanded because' + + ' the "configObjectFullPath" setting was not specified', + ); + } + + // "The default value for `projectFolder` is the token ``, which means the folder is determined + // by traversing parent folders, starting from the folder containing api-extractor.json, and stopping + // at the first folder that contains a tsconfig.json file. If a tsconfig.json file cannot be found in + // this way, then an error will be reported." + + let currentFolder: string = path.dirname(options.configObjectFullPath); + for (;;) { + const tsconfigPath: string = path.join(currentFolder, 'tsconfig.json'); + if (FileSystem.exists(tsconfigPath)) { + projectFolder = currentFolder; + break; + } + + const parentFolder: string = path.dirname(currentFolder); + if (parentFolder === '' || parentFolder === currentFolder) { + throw new Error( + 'The "projectFolder" setting uses the "" token, but a tsconfig.json file cannot be' + + ' found in this folder or any parent folder.', + ); + } + + currentFolder = parentFolder; + } + } + } else { + ExtractorConfig._rejectAnyTokensInPath(configObject.projectFolder, 'projectFolder'); + + if (!FileSystem.exists(configObject.projectFolder)) { + throw new Error('The specified "projectFolder" path does not exist: ' + configObject.projectFolder); + } + + projectFolder = configObject.projectFolder; + } + + const tokenContext: IExtractorConfigTokenContext = { + unscopedPackageName: 'unknown-package', + packageName: 'unknown-package', + projectFolder, + }; + + if (packageJson) { + tokenContext.packageName = packageJson.name; + tokenContext.unscopedPackageName = PackageName.getUnscopedName(packageJson.name); + } + + if (!configObject.mainEntryPointFilePath) { + // A merged configuration should have this + throw new Error('The "mainEntryPointFilePath" setting is missing'); + } + + const mainEntryPointFilePath: string = ExtractorConfig._resolvePathWithTokens( + 'mainEntryPointFilePath', + configObject.mainEntryPointFilePath, + tokenContext, + ); + + if (!ExtractorConfig.hasDtsFileExtension(mainEntryPointFilePath)) { + throw new Error('The "mainEntryPointFilePath" value is not a declaration file: ' + mainEntryPointFilePath); + } + + if (!options.ignoreMissingEntryPoint && !FileSystem.exists(mainEntryPointFilePath)) { + throw new Error('The "mainEntryPointFilePath" path does not exist: ' + mainEntryPointFilePath); + } + + const bundledPackages: string[] = configObject.bundledPackages ?? []; + for (const bundledPackage of bundledPackages) { + if (!PackageName.isValidName(bundledPackage)) { + throw new Error(`The "bundledPackages" list contains an invalid package name: "${bundledPackage}"`); + } + } + + const tsconfigFilePath: string = ExtractorConfig._resolvePathWithTokens( + 'tsconfigFilePath', + configObject.compiler.tsconfigFilePath, + tokenContext, + ); + + if (configObject.compiler.overrideTsconfig === undefined) { + if (!tsconfigFilePath) { + throw new Error('Either the "tsconfigFilePath" or "overrideTsconfig" setting must be specified'); + } + + if (!FileSystem.exists(tsconfigFilePath)) { + throw new Error('The file referenced by "tsconfigFilePath" does not exist: ' + tsconfigFilePath); + } + } + + 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); + } + + let docModelEnabled = false; + let apiJsonFilePath = ''; + let docModelIncludeForgottenExports = false; + let projectFolderUrl: string | undefined; + if (configObject.docModel) { + docModelEnabled = Boolean(configObject.docModel.enabled); + apiJsonFilePath = ExtractorConfig._resolvePathWithTokens( + 'apiJsonFilePath', + configObject.docModel.apiJsonFilePath, + tokenContext, + ); + docModelIncludeForgottenExports = Boolean(configObject.docModel.includeForgottenExports); + projectFolderUrl = configObject.docModel.projectFolderUrl; + } + + let tsdocMetadataEnabled = false; + let tsdocMetadataFilePath = ''; + if (configObject.tsdocMetadata) { + tsdocMetadataEnabled = Boolean(configObject.tsdocMetadata.enabled); + + if (tsdocMetadataEnabled) { + tsdocMetadataFilePath = configObject.tsdocMetadata.tsdocMetadataFilePath ?? ''; + + if (tsdocMetadataFilePath.trim() === '') { + if (!packageJson) { + throw new Error( + 'The "" token cannot be used with the "tsdocMetadataFilePath" setting because' + + ' the "packageJson" option was not provided', + ); + } + + if (!packageJsonFullPath) { + throw new Error( + 'The "" token cannot be used with "tsdocMetadataFilePath" because' + + 'the "packageJsonFullPath" option was not provided', + ); + } + + tsdocMetadataFilePath = PackageMetadataManager.resolveTsdocMetadataPath( + path.dirname(packageJsonFullPath), + packageJson, + ); + } else { + tsdocMetadataFilePath = ExtractorConfig._resolvePathWithTokens( + 'tsdocMetadataFilePath', + configObject.tsdocMetadata.tsdocMetadataFilePath, + tokenContext, + ); + } + + if (!tsdocMetadataFilePath) { + throw new Error( + 'The "tsdocMetadata.enabled" setting is enabled, but "tsdocMetadataFilePath" is not specified', + ); + } + } + } + + let rollupEnabled = false; + let untrimmedFilePath = ''; + let betaTrimmedFilePath = ''; + let alphaTrimmedFilePath = ''; + let publicTrimmedFilePath = ''; + let omitTrimmingComments = false; + + if (configObject.dtsRollup) { + rollupEnabled = Boolean(configObject.dtsRollup.enabled); + untrimmedFilePath = ExtractorConfig._resolvePathWithTokens( + 'untrimmedFilePath', + configObject.dtsRollup.untrimmedFilePath, + tokenContext, + ); + alphaTrimmedFilePath = ExtractorConfig._resolvePathWithTokens( + 'alphaTrimmedFilePath', + configObject.dtsRollup.alphaTrimmedFilePath, + tokenContext, + ); + betaTrimmedFilePath = ExtractorConfig._resolvePathWithTokens( + 'betaTrimmedFilePath', + configObject.dtsRollup.betaTrimmedFilePath, + tokenContext, + ); + publicTrimmedFilePath = ExtractorConfig._resolvePathWithTokens( + 'publicTrimmedFilePath', + configObject.dtsRollup.publicTrimmedFilePath, + tokenContext, + ); + omitTrimmingComments = Boolean(configObject.dtsRollup.omitTrimmingComments); + } + + let newlineKind: NewlineKind; + switch (configObject.newlineKind) { + case 'lf': + newlineKind = NewlineKind.Lf; + break; + case 'os': + newlineKind = NewlineKind.OsDefault; + break; + default: + newlineKind = NewlineKind.CrLf; + break; + } + + const enumMemberOrder: EnumMemberOrder = configObject.enumMemberOrder ?? EnumMemberOrder.ByName; + + extractorConfigParameters = { + projectFolder, + packageJson, + packageFolder, + mainEntryPointFilePath, + bundledPackages, + tsconfigFilePath, + overrideTsconfig: configObject.compiler.overrideTsconfig, + skipLibCheck: Boolean(configObject.compiler.skipLibCheck), + apiReportEnabled, + reportFilePath, + reportTempFilePath, + apiReportIncludeForgottenExports, + docModelEnabled, + apiJsonFilePath, + docModelIncludeForgottenExports, + projectFolderUrl, + rollupEnabled, + untrimmedFilePath, + alphaTrimmedFilePath, + betaTrimmedFilePath, + publicTrimmedFilePath, + omitTrimmingComments, + tsdocMetadataEnabled, + tsdocMetadataFilePath, + newlineKind, + messages: configObject.messages ?? {}, + testMode: Boolean(configObject.testMode), + enumMemberOrder, + }; + } catch (error) { + throw new Error(`Error parsing ${filenameForErrors}:\n` + (error as Error).message); + } + + let tsdocConfigFile: TSDocConfigFile | undefined = options.tsdocConfigFile; + + if (!tsdocConfigFile) { + // Example: "my-project/tsdoc.json" + let packageTSDocConfigPath: string = TSDocConfigFile.findConfigPathForFolder( + extractorConfigParameters.projectFolder, + ); + + if (!packageTSDocConfigPath || !FileSystem.exists(packageTSDocConfigPath)) { + // If the project does not have a tsdoc.json config file, then use API Extractor's base file. + packageTSDocConfigPath = ExtractorConfig._tsdocBaseFilePath; + if (!FileSystem.exists(packageTSDocConfigPath)) { + throw new InternalError('Unable to load the built-in TSDoc config file: ' + packageTSDocConfigPath); + } + } + + tsdocConfigFile = TSDocConfigFile.loadFile(packageTSDocConfigPath); + } + + // IMPORTANT: After calling TSDocConfigFile.loadFile(), we need to check for errors. + if (tsdocConfigFile.hasErrors) { + throw new Error(tsdocConfigFile.getErrorSummary()); + } + + const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration(); + tsdocConfigFile.configureParser(tsdocConfiguration); + + // IMPORTANT: After calling TSDocConfigFile.configureParser(), we need to check for errors a second time. + if (tsdocConfigFile.hasErrors) { + throw new Error(tsdocConfigFile.getErrorSummary()); + } + + return new ExtractorConfig({ ...extractorConfigParameters, tsdocConfigFile, tsdocConfiguration }); + } + + private static _resolvePathWithTokens( + fieldName: string, + value: string | undefined, + tokenContext: IExtractorConfigTokenContext, + ): string { + const returnValue = ExtractorConfig._expandStringWithTokens(fieldName, value, tokenContext); + if (returnValue !== '') { + return path.resolve(tokenContext.projectFolder, returnValue); + } + + return returnValue; + } + + private static _expandStringWithTokens( + fieldName: string, + value: string | undefined, + tokenContext: IExtractorConfigTokenContext, + ): string { + let returnValue = value ? value.trim() : ''; + if (returnValue !== '') { + returnValue = Text.replaceAll(returnValue, '', tokenContext.unscopedPackageName); + returnValue = Text.replaceAll(returnValue, '', tokenContext.packageName); + + const projectFolderToken = ''; + if (returnValue.startsWith(projectFolderToken)) { + // Replace "" at the start of a string + returnValue = path.join(tokenContext.projectFolder, returnValue.slice(projectFolderToken.length)); + } + + if (returnValue.includes(projectFolderToken)) { + // If after all replacements, "" appears somewhere in the string, report an error + throw new Error( + `The "${fieldName}" value incorrectly uses the "" token.` + + ` It must appear at the start of the string.`, + ); + } + + if (returnValue.includes('')) { + throw new Error(`The "${fieldName}" value incorrectly uses the "" token`); + } + + ExtractorConfig._rejectAnyTokensInPath(returnValue, fieldName); + } + + return returnValue; + } + + /** + * Returns true if the specified file path has the ".d.ts" file extension. + */ + public static hasDtsFileExtension(filePath: string): boolean { + return ExtractorConfig._declarationFileExtensionRegExp.test(filePath); + } + + /** + * Given a path string that may have originally contained expandable tokens such as `"` + * this reports an error if any token-looking substrings remain after expansion (e.g. `c:\blah\\blah`). + */ + private static _rejectAnyTokensInPath(value: string, fieldName: string): void { + if (!value.includes('<') && !value.includes('>')) { + return; + } + + // Can we determine the name of a token? + const tokenRegExp = /(?<[^<]*?>)/; + const match: RegExpExecArray | null = tokenRegExp.exec(value); + if (match?.groups?.token) { + throw new Error(`The "${fieldName}" value contains an unrecognized token "${match.groups.token}"`); + } + + throw new Error(`The "${fieldName}" value contains extra token characters ("<" or ">"): ${value}`); + } +} diff --git a/packages/api-extractor/src/api/ExtractorLogLevel.ts b/packages/api-extractor/src/api/ExtractorLogLevel.ts new file mode 100644 index 000000000..48dc1ee73 --- /dev/null +++ b/packages/api-extractor/src/api/ExtractorLogLevel.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Used with {@link IConfigMessageReportingRule.logLevel} and {@link IExtractorInvokeOptions.messageCallback}. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export const enum ExtractorLogLevel { + /** + * The message will be displayed as an error. + * + * @remarks + * Errors typically cause the build to fail and return a nonzero exit code. + */ + Error = 'error', + + /** + * The message will be displayed as an informational message. + * + * @remarks + * Informational messages may contain newlines to ensure nice formatting of the output, + * however word-wrapping is the responsibility of the message handler. + */ + Info = 'info', + + /** + * The message will be discarded entirely. + */ + None = 'none', + + /** + * The message will be displayed only when "verbose" output is requested, e.g. using the `--verbose` + * command line option. + */ + Verbose = 'verbose', + + /** + * The message will be displayed as an warning. + * + * @remarks + * Warnings typically cause a production build fail and return a nonzero exit code. For a non-production build + * (e.g. using the `--local` option with `api-extractor run`), the warning is displayed but the build will not fail. + */ + Warning = 'warning', +} diff --git a/packages/api-extractor/src/api/ExtractorMessage.ts b/packages/api-extractor/src/api/ExtractorMessage.ts new file mode 100644 index 000000000..e2c45c234 --- /dev/null +++ b/packages/api-extractor/src/api/ExtractorMessage.ts @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js'; +import type { ConsoleMessageId } from './ConsoleMessageId.js'; +import { ExtractorLogLevel } from './ExtractorLogLevel.js'; +import type { ExtractorMessageId } from './ExtractorMessageId.js'; + +/** + * Used by {@link ExtractorMessage.properties}. + * + * @public + */ +export interface IExtractorMessageProperties { + /** + * A declaration can have multiple names if it is exported more than once. + * If an `ExtractorMessage` applies to a specific export name, this property can indicate that. + * + * @remarks + * + * Used by {@link ExtractorMessageId.InternalMissingUnderscore}. + */ + readonly exportName?: string; +} + +/** + * Specifies a category of messages for use with {@link ExtractorMessage}. + * + * @public + */ +export const enum ExtractorMessageCategory { + /** + * Messages originating from the TypeScript compiler. + * + * @remarks + * These strings begin with the prefix "TS" and have a numeric error code. + * Example: `TS2551` + */ + Compiler = 'Compiler', + + /** + * Console messages communicate the progress of the overall operation. They may include newlines to ensure + * nice formatting. They are output in real time, and cannot be routed to the API Report file. + * + * @remarks + * These strings begin with the prefix "console-". + * Example: `console-writing-typings-file` + */ + Console = 'console', + + /** + * Messages related to API Extractor's analysis. + * + * @remarks + * These strings begin with the prefix "ae-". + * Example: `ae-extra-release-tag` + */ + Extractor = 'Extractor', + + /** + * Messages related to parsing of TSDoc comments. + * + * @remarks + * These strings begin with the prefix "tsdoc-". + * Example: `tsdoc-link-tag-unescaped-text` + */ + TSDoc = 'TSDoc', +} + +/** + * Constructor options for `ExtractorMessage`. + */ +export interface IExtractorMessageOptions { + category: ExtractorMessageCategory; + logLevel?: ExtractorLogLevel; + messageId: ConsoleMessageId | ExtractorMessageId | tsdoc.TSDocMessageId | string; + properties?: IExtractorMessageProperties | undefined; + sourceFileColumn?: number; + sourceFileLine?: number; + sourceFilePath?: string; + text: string; +} + +/** + * This object is used to report an error or warning that occurred during API Extractor's analysis. + * + * @public + */ +export class ExtractorMessage { + private _handled: boolean; + + private _logLevel: ExtractorLogLevel; + + /** + * The category of issue. + */ + public readonly category: ExtractorMessageCategory; + + /** + * A text string that uniquely identifies the issue type. This identifier can be used to suppress + * or configure the reporting of issues, and also to search for help about an issue. + */ + public readonly messageId: ConsoleMessageId | ExtractorMessageId | tsdoc.TSDocMessageId | string; + + /** + * The text description of this issue. + */ + public readonly text: string; + + /** + * The absolute path to the affected input source file, if there is one. + */ + public readonly sourceFilePath: string | undefined; + + /** + * The line number where the issue occurred in the input source file. This is not used if `sourceFilePath` + * is undefined. The first line number is 1. + */ + public readonly sourceFileLine: number | undefined; + + /** + * The column number where the issue occurred in the input source file. This is not used if `sourceFilePath` + * is undefined. The first column number is 1. + */ + public readonly sourceFileColumn: number | undefined; + + /** + * Additional contextual information about the message that may be useful when reporting errors. + * All properties are optional. + */ + public readonly properties: IExtractorMessageProperties; + + /** + * @internal + */ + public constructor(options: IExtractorMessageOptions) { + this.category = options.category; + this.messageId = options.messageId; + this.text = options.text; + this.sourceFilePath = options.sourceFilePath; + this.sourceFileLine = options.sourceFileLine; + this.sourceFileColumn = options.sourceFileColumn; + this.properties = options.properties ?? {}; + + this._handled = false; + this._logLevel = options.logLevel ?? ExtractorLogLevel.None; + } + + /** + * If the {@link IExtractorInvokeOptions.messageCallback} sets this property to true, it will prevent the message + * from being displayed by API Extractor. + * + * @remarks + * If the `messageCallback` routes the message to a custom handler (e.g. a toolchain logger), it should + * assign `handled = true` to prevent API Extractor from displaying it. Assigning `handled = true` for all messages + * would effectively disable all console output from the `Extractor` API. + * + * If `handled` is set to true, the message will still be included in the count of errors/warnings; + * to discard a message entirely, instead assign `logLevel = none`. + */ + public get handled(): boolean { + return this._handled; + } + + public set handled(value: boolean) { + if (this._handled && !value) { + throw new Error('One a message has been marked as handled, the "handled" property cannot be set to false'); + } + + this._handled = value; + } + + /** + * Specifies how the message should be reported. + * + * @remarks + * If the {@link IExtractorInvokeOptions.messageCallback} handles the message (i.e. sets `handled = true`), + * it can use the `logLevel` to determine how to display the message. + * + * Alternatively, if API Extractor is handling the message, the `messageCallback` could assign `logLevel` to change + * how it will be processed. However, in general the recommended practice is to configure message routing + * using the `messages` section in api-extractor.json. + * + * To discard a message entirely, assign `logLevel = none`. + */ + public get logLevel(): ExtractorLogLevel { + return this._logLevel; + } + + public set logLevel(value: ExtractorLogLevel) { + switch (value) { + case ExtractorLogLevel.Error: + case ExtractorLogLevel.Info: + case ExtractorLogLevel.None: + case ExtractorLogLevel.Verbose: + case ExtractorLogLevel.Warning: + break; + default: + throw new Error('Invalid log level'); + } + + this._logLevel = value; + } + + /** + * Returns the message formatted with its identifier and file position. + * + * @remarks + * Example: + * ``` + * src/folder/File.ts:123:4 - (ae-extra-release-tag) The doc comment should not contain more than one release tag. + * ``` + */ + public formatMessageWithLocation(workingPackageFolderPath: string | undefined): string { + let result = ''; + + if (this.sourceFilePath) { + result += SourceFileLocationFormatter.formatPath(this.sourceFilePath, { + sourceFileLine: this.sourceFileLine, + sourceFileColumn: this.sourceFileColumn, + workingPackageFolderPath, + }); + + if (result.length > 0) { + result += ' - '; + } + } + + result += this.formatMessageWithoutLocation(); + + return result; + } + + public formatMessageWithoutLocation(): string { + return `(${this.messageId}) ${this.text}`; + } +} diff --git a/packages/api-extractor/src/api/ExtractorMessageId.ts b/packages/api-extractor/src/api/ExtractorMessageId.ts new file mode 100644 index 000000000..094f748fa --- /dev/null +++ b/packages/api-extractor/src/api/ExtractorMessageId.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Unique identifiers for messages reported by API Extractor during its analysis. + * + * @remarks + * + * These strings are possible values for the {@link ExtractorMessage.messageId} property + * when the `ExtractorMessage.category` is {@link ExtractorMessageCategory.Extractor}. + * @public + */ +export const enum ExtractorMessageId { + /** + * "The `@inheritDoc` tag for ___ refers to its own declaration." + */ + CyclicInheritDoc = 'ae-cyclic-inherit-doc', + + /** + * "This symbol has another declaration with a different release tag." + */ + DifferentReleaseTags = 'ae-different-release-tags', + + /** + * "The doc comment should not contain more than one release tag." + */ + ExtraReleaseTag = 'ae-extra-release-tag', + + /** + * "The symbol ___ needs to be exported by the entry point ___." + */ + ForgottenExport = 'ae-forgotten-export', + + /** + * "The symbol ___ is marked as ___, but its signature references ___ which is marked as ___." + */ + IncompatibleReleaseTags = 'ae-incompatible-release-tags', + + /** + * "The name ___ should be prefixed with an underscore because the declaration is marked as `@internal`." + */ + InternalMissingUnderscore = 'ae-internal-missing-underscore', + + /** + * "Mixed release tags are not allowed for ___ because one of its declarations is marked as `@internal`." + */ + InternalMixedReleaseTag = 'ae-internal-mixed-release-tag', + + /** + * "The `@packageDocumentation` comment must appear at the top of entry point *.d.ts file." + */ + MisplacedPackageTag = 'ae-misplaced-package-tag', + + /** + * "The property ___ has a setter but no getter." + */ + MissingGetter = 'ae-missing-getter', + + /** + * "___ is part of the package's API, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)." + */ + MissingReleaseTag = 'ae-missing-release-tag', + + /** + * "The `@preapproved` tag cannot be applied to ___ without an `@internal` release tag." + */ + PreapprovedBadReleaseTag = 'ae-preapproved-bad-release-tag', + + /** + * "The `@preapproved` tag cannot be applied to ___ because it is not a supported declaration type." + */ + PreapprovedUnsupportedType = 'ae-preapproved-unsupported-type', + + /** + * "The doc comment for the property ___ must appear on the getter, not the setter." + */ + SetterWithDocs = 'ae-setter-with-docs', + + /** + * "Missing documentation for ___." + * + * @remarks + * The `ae-undocumented` message is only generated if the API report feature is enabled. + * + * Because the API report file already annotates undocumented items with `// (undocumented)`, + * the `ae-undocumented` message is not logged by default. To see it, add a setting such as: + * ```json + * "messages": { + * "extractorMessageReporting": { + * "ae-undocumented": { + * "logLevel": "warning" + * } + * } + * } + * ``` + */ + Undocumented = 'ae-undocumented', + + /** + * "The `@inheritDoc` tag needs a TSDoc declaration reference; signature matching is not supported yet." + * + * @privateRemarks + * In the future, we will implement signature matching so that you can write `{@inheritDoc}` and API Extractor + * will find a corresponding member from a base class (or implemented interface). Until then, the tag + * always needs an explicit declaration reference such as `{@inhertDoc MyBaseClass.sameMethod}`. + */ + UnresolvedInheritDocBase = 'ae-unresolved-inheritdoc-base', + + /** + * "The `@inheritDoc` reference could not be resolved." + */ + UnresolvedInheritDocReference = 'ae-unresolved-inheritdoc-reference', + + /** + * "The `@link` reference could not be resolved." + */ + UnresolvedLink = 'ae-unresolved-link', + + /** + * "Incorrect file type; API Extractor expects to analyze compiler outputs with the .d.ts file extension. + * Troubleshooting tips: `https://api-extractor.com/link/dts-error`" + */ + WrongInputFileType = 'ae-wrong-input-file-type', +} + +export const allExtractorMessageIds: Set = new Set([ + 'ae-extra-release-tag', + 'ae-undocumented', + 'ae-different-release-tags', + 'ae-incompatible-release-tags', + 'ae-missing-release-tag', + 'ae-misplaced-package-tag', + 'ae-forgotten-export', + 'ae-internal-missing-underscore', + 'ae-internal-mixed-release-tag', + 'ae-preapproved-unsupported-type', + 'ae-preapproved-bad-release-tag', + 'ae-unresolved-inheritdoc-reference', + 'ae-unresolved-inheritdoc-base', + 'ae-cyclic-inherit-doc', + 'ae-unresolved-link', + 'ae-setter-with-docs', + 'ae-missing-getter', + 'ae-wrong-input-file-type', +]); diff --git a/packages/api-extractor/src/api/IConfigFile.ts b/packages/api-extractor/src/api/IConfigFile.ts new file mode 100644 index 000000000..4e014d56e --- /dev/null +++ b/packages/api-extractor/src/api/IConfigFile.ts @@ -0,0 +1,448 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { EnumMemberOrder } from '@discordjs/api-extractor-model'; +import type { ExtractorLogLevel } from './ExtractorLogLevel.js'; + +/** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigCompiler { + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * + * @remarks + * The value must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will instead be read from the projectFolder. + */ + overrideTsconfig?: {}; + + /** + * This option causes the compiler to be invoked with the `--skipLibCheck` option. + * + * @remarks + * This option is not recommended and may cause API Extractor to produce incomplete or incorrect declarations, + * but it may be required when dependencies contain declarations that are incompatible with the TypeScript engine + * that API Extractor uses for its analysis. Where possible, the underlying issue should be fixed rather than + * relying on skipLibCheck. + */ + skipLibCheck?: boolean; + + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * @remarks + * 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 ``. + * + * Note: This setting will be ignored if `overrideTsconfig` is used. + */ + tsconfigFilePath?: string; +} + +/** + * Configures how the API report files (*.api.md) will be generated. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigApiReport { + /** + * Whether to generate an API report. + */ + enabled: boolean; + + /** + * Whether "forgotten exports" should be included in the API report file. + * + * @remarks + * Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See + * https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more. + * @defaultValue `false` + */ + includeForgottenExports?: boolean; + + /** + * The filename for the API report files. It will be combined with `reportFolder` or `reportTempFolder` to produce + * a full output filename. + * + * @remarks + * The file extension should be ".api.md", and the string should not contain a path separator such as `\` or `/`. + */ + reportFileName?: string; + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the `reportFileName` setting. + * + * @remarks + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * 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 ``. + */ + reportFolder?: string; + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the `reportFileName` setting. + * + * @remarks + * After the temporary file is written to disk, it is compared with the file in the `reportFolder`. + * If they are different, a production build will fail. + * + * 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 ``. + */ + reportTempFolder?: string; +} + +/** + * Configures how the doc model file (*.api.json) will be generated. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigDocModel { + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * @remarks + * 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 ``. + */ + apiJsonFilePath?: string; + + /** + * Whether to generate a doc model file. + */ + enabled: boolean; + + /** + * Whether "forgotten exports" should be included in the doc model file. + * + * @remarks + * Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See + * https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more. + * @defaultValue `false` + */ + includeForgottenExports?: boolean; + + /** + * 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 `` path on disk. + * + * @remarks + * 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. + */ + projectFolderUrl?: string; +} + +/** + * Configures how the .d.ts rollup file will be generated. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigDtsRollup { + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for an "alpha" release. + * + * @remarks + * This file will include only declarations that are marked as `@public`, `@beta`, or `@alpha`. + * + * 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 ``. + */ + alphaTrimmedFilePath?: string; + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * + * @remarks + * This file will include only declarations that are marked as `@public` or `@beta`. + * + * 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 ``. + */ + betaTrimmedFilePath?: string; + + /** + * Whether to generate the .d.ts rollup file. + */ + enabled: boolean; + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + */ + omitTrimmingComments?: boolean; + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * + * @remarks + * This file will include only declarations that are marked as `@public`. + * + * 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 ``. + */ + publicTrimmedFilePath?: string; + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * + * @remarks + * This file will include all declarations that are exported by the main entry point. + * + * 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 ``. + */ + untrimmedFilePath?: string; +} + +/** + * Configures how the tsdoc-metadata.json file will be generated. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigTsdocMetadata { + /** + * Whether to generate the tsdoc-metadata.json file. + */ + enabled: boolean; + + /** + * Specifies where the TSDoc metadata file should be written. + * + * @remarks + * 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 ``. + * + * The default value is ``, which causes the path to be automatically inferred from the `tsdocMetadata`, + * `typings` or `main` fields of the project's package.json. If none of these fields are set, the lookup + * falls back to `tsdoc-metadata.json` in the package folder. + */ + tsdocMetadataFilePath?: string; +} + +/** + * Configures reporting for a given message identifier. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigMessageReportingRule { + /** + * When `addToApiReportFile` is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the `logLevel` option. + */ + addToApiReportFile?: boolean; + + /** + * Specifies whether the message should be written to the the tool's output log. + * + * @remarks + * Note that the `addToApiReportFile` property may supersede this option. + */ + logLevel: ExtractorLogLevel; +} + +/** + * Specifies a table of reporting rules for different message identifiers, and also the default rule used for + * identifiers that do not appear in the table. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IConfigMessageReportingTable { + /** + * The key is a message identifier for the associated type of message, or "default" to specify the default policy. + * For example, the key might be `TS2551` (a compiler message), `tsdoc-link-tag-unescaped-text` (a TSDOc message), + * or `ae-extra-release-tag` (a message related to the API Extractor analysis). + */ + [messageId: string]: IConfigMessageReportingRule; +} + +/** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * @remarks + * This is part of the {@link IConfigFile} structure. + * @public + */ +export interface IExtractorMessagesConfig { + /** + * Configures handling of diagnostic messages generating the TypeScript compiler while analyzing the + * input .d.ts files. + */ + compilerMessageReporting?: IConfigMessageReportingTable; + + /** + * Configures handling of messages reported by API Extractor during its analysis. + */ + extractorMessageReporting?: IConfigMessageReportingTable; + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + */ + tsdocMessageReporting?: IConfigMessageReportingTable; +} + +/** + * Configuration options for the API Extractor tool. These options can be constructed programmatically + * or loaded from the api-extractor.json config file using the {@link ExtractorConfig} class. + * + * @public + */ +export interface IConfigFile { + /** + * {@inheritDoc IConfigApiReport} + */ + apiReport?: IConfigApiReport; + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * @remarks + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project `library1`, + * and another NPM package `library2` is embedded in this bundle. Some types from `library2` may become part + * of the exported API for `library1`, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports `library2`. To avoid this, we can specify: + * + * ```js + * "bundledPackages": [ "library2" ], + * ``` + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for `library1`. + */ + bundledPackages?: string[]; + + /** + * {@inheritDoc IConfigCompiler} + */ + compiler?: IConfigCompiler; + + /** + * {@inheritDoc IConfigDocModel} + */ + docModel?: IConfigDocModel; + + /** + * {@inheritDoc IConfigDtsRollup} + * + * @beta + */ + dtsRollup?: IConfigDtsRollup; + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. + * + * @remarks + * By default, the output files will be sorted alphabetically, which is "by-name". + * To keep the ordering in the source code, specify "preserve". + * @defaultValue `by-name` + */ + enumMemberOrder?: EnumMemberOrder; + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * @remarks + * If the path starts with `./` or `../`, the path is resolved relative to the folder of the file that contains + * the `extends` field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS `require()`. + */ + extends?: string; + + /** + * Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * @remarks + * + * The file extension must be ".d.ts" and not ".ts". + * The path is resolved relative to the "projectFolder" location. + */ + mainEntryPointFilePath: string; + + /** + * {@inheritDoc IExtractorMessagesConfig} + */ + messages?: IExtractorMessagesConfig; + + /** + * Specifies what type of newlines API Extractor should use when writing output files. + * + * @remarks + * By default, the output files will be written with Windows-style newlines. + * To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + */ + newlineKind?: 'crlf' | 'lf' | 'os'; + + /** + * Determines the `` token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * @remarks + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for `projectFolder` is the token ``, which means the folder is determined using + * the following heuristics: + * + * If the config/rig.json system is used (as defined by {@link https://www.npmjs.com/package/@rushstack/rig-package + * | @rushstack/rig-package}), then the `` value will be the package folder that referenced the rig. + * + * Otherwise, the `` value is determined by traversing parent folders, starting from the folder containing + * api-extractor.json, and stopping at the first folder that contains a tsconfig.json file. If a tsconfig.json file + * cannot be found in this way, then an error will be reported. + */ + projectFolder?: string; + + /** + * Set to true when invoking API Extractor's test harness. + * + * @remarks + * When `testMode` is true, the `toolVersion` field in the .api.json file is assigned an empty string + * to prevent spurious diffs in output files tracked for tests. + */ + testMode?: boolean; + + /** + * {@inheritDoc IConfigTsdocMetadata} + * + * @beta + */ + tsdocMetadata?: IConfigTsdocMetadata; +} diff --git a/packages/api-extractor/src/api/test/Extractor-custom-tags.test.ts b/packages/api-extractor/src/api/test/Extractor-custom-tags.test.ts new file mode 100644 index 000000000..ca7fcae4b --- /dev/null +++ b/packages/api-extractor/src/api/test/Extractor-custom-tags.test.ts @@ -0,0 +1,55 @@ +// 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)); + } + }); + }); +}); diff --git a/packages/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts b/packages/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts new file mode 100644 index 000000000..cf7aa14b2 --- /dev/null +++ b/packages/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts @@ -0,0 +1,36 @@ +// 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 "" 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/')); + }); +}); diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup1/api-extractor.json b/packages/api-extractor/src/api/test/test-data/config-lookup1/api-extractor.json new file mode 100644 index 000000000..e0be3acb7 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup1/api-extractor.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup1/index.d.ts b/packages/api-extractor/src/api/test/test-data/config-lookup1/index.d.ts new file mode 100644 index 000000000..54db3686d --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup1/index.d.ts @@ -0,0 +1,2 @@ +/* eslint-disable unicorn/no-empty-file */ +// empty file diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup1/package.json b/packages/api-extractor/src/api/test/test-data/config-lookup1/package.json new file mode 100644 index 000000000..f58ac86ae --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup1/package.json @@ -0,0 +1,4 @@ +{ + "name": "config-lookup1", + "version": "1.0.0" +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup1/tsconfig.json b/packages/api-extractor/src/api/test/test-data/config-lookup1/tsconfig.json new file mode 100644 index 000000000..d71df8e8f --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup1/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$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"] +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup2/config/api-extractor.json b/packages/api-extractor/src/api/test/test-data/config-lookup2/config/api-extractor.json new file mode 100644 index 000000000..e0be3acb7 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup2/config/api-extractor.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup2/index.d.ts b/packages/api-extractor/src/api/test/test-data/config-lookup2/index.d.ts new file mode 100644 index 000000000..54db3686d --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup2/index.d.ts @@ -0,0 +1,2 @@ +/* eslint-disable unicorn/no-empty-file */ +// empty file diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup2/package.json b/packages/api-extractor/src/api/test/test-data/config-lookup2/package.json new file mode 100644 index 000000000..f8c81ed84 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup2/package.json @@ -0,0 +1,4 @@ +{ + "name": "config-lookup2", + "version": "1.0.0" +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup2/tsconfig.json b/packages/api-extractor/src/api/test/test-data/config-lookup2/tsconfig.json new file mode 100644 index 000000000..d71df8e8f --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup2/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$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"] +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/config/api-extractor.json b/packages/api-extractor/src/api/test/test-data/config-lookup3/config/api-extractor.json new file mode 100644 index 000000000..e0be3acb7 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/config/api-extractor.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/index.d.ts b/packages/api-extractor/src/api/test/test-data/config-lookup3/index.d.ts new file mode 100644 index 000000000..54db3686d --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/index.d.ts @@ -0,0 +1,2 @@ +/* eslint-disable unicorn/no-empty-file */ +// empty file diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/package.json b/packages/api-extractor/src/api/test/test-data/config-lookup3/package.json new file mode 100644 index 000000000..f262956ca --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/package.json @@ -0,0 +1,4 @@ +{ + "name": "config-lookup3", + "version": "1.0.0" +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/config/api-extractor.json b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/config/api-extractor.json new file mode 100644 index 000000000..e0be3acb7 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/config/api-extractor.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/index.d.ts b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/index.d.ts new file mode 100644 index 000000000..54db3686d --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/index.d.ts @@ -0,0 +1,2 @@ +/* eslint-disable unicorn/no-empty-file */ +// empty file diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/tsconfig.json b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/tsconfig.json new file mode 100644 index 000000000..d71df8e8f --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/src/test/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$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"] +} diff --git a/packages/api-extractor/src/api/test/test-data/config-lookup3/tsconfig.json b/packages/api-extractor/src/api/test/test-data/config-lookup3/tsconfig.json new file mode 100644 index 000000000..d71df8e8f --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/config-lookup3/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$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"] +} diff --git a/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/api-extractor.json b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/api-extractor.json new file mode 100644 index 000000000..e0be3acb7 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/api-extractor.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/index.d.ts", + + "apiReport": { + "enabled": true + }, + + "docModel": { + "enabled": true + }, + + "dtsRollup": { + "enabled": true + } +} diff --git a/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/index.d.ts b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/index.d.ts new file mode 100644 index 000000000..aa4ed12e9 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/index.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/no-empty-interface,tsdoc/syntax */ +/** + * @block + * @inline test + * @modifier + */ +interface CustomTagsTestInterface {} diff --git a/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/package.json b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/package.json new file mode 100644 index 000000000..f58ac86ae --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/package.json @@ -0,0 +1,4 @@ +{ + "name": "config-lookup1", + "version": "1.0.0" +} diff --git a/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsconfig.json b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsconfig.json new file mode 100644 index 000000000..d71df8e8f --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$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"] +} diff --git a/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsdoc.json b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsdoc.json new file mode 100644 index 000000000..817d22555 --- /dev/null +++ b/packages/api-extractor/src/api/test/test-data/custom-tsdoc-tags/tsdoc.json @@ -0,0 +1,24 @@ +{ + "$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 + } +} diff --git a/packages/api-extractor/src/cli/ApiExtractorCommandLine.ts b/packages/api-extractor/src/cli/ApiExtractorCommandLine.ts new file mode 100644 index 000000000..785e567fc --- /dev/null +++ b/packages/api-extractor/src/cli/ApiExtractorCommandLine.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as os from 'node:os'; +import { InternalError } from '@rushstack/node-core-library'; +import { CommandLineParser, type CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import colors from 'colors'; +import { InitAction } from './InitAction.js'; +import { RunAction } from './RunAction.js'; + +export class ApiExtractorCommandLine extends CommandLineParser { + private readonly _debugParameter: CommandLineFlagParameter; + + public constructor() { + super({ + toolFilename: 'api-extractor', + toolDescription: + 'API Extractor helps you build better TypeScript libraries. It analyzes the main entry' + + ' point for your package, collects the inventory of exported declarations, and then generates three kinds' + + ' of output: an API report file (.api.md) to facilitate reviews, a declaration rollup (.d.ts) to be' + + ' published with your NPM package, and a doc model file (.api.json) to be used with a documentation' + + ' tool such as api-documenter. For details, please visit the web site.', + }); + this._populateActions(); + + this._debugParameter = this.defineFlagParameter({ + parameterLongName: '--debug', + parameterShortName: '-d', + description: 'Show the full call stack if an error occurs while executing the tool', + }); + } + + protected override async onExecute(): Promise { + // override + if (this._debugParameter.value) { + InternalError.breakInDebugger = true; + } + + try { + await super.onExecute(); + } 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, n/prefer-global/process + process.exitCode = 1; + } + } + + private _populateActions(): void { + this.addAction(new InitAction(this)); + this.addAction(new RunAction(this)); + } +} diff --git a/packages/api-extractor/src/cli/InitAction.ts b/packages/api-extractor/src/cli/InitAction.ts new file mode 100644 index 000000000..63e87c9dd --- /dev/null +++ b/packages/api-extractor/src/cli/InitAction.ts @@ -0,0 +1,45 @@ +// 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 } from '@rushstack/node-core-library'; +import { CommandLineAction } from '@rushstack/ts-command-line'; +import colors from 'colors'; +import { ExtractorConfig } from '../api/ExtractorConfig.js'; +import type { ApiExtractorCommandLine } from './ApiExtractorCommandLine.js'; + +export class InitAction extends CommandLineAction { + public constructor(_parser: ApiExtractorCommandLine) { + super({ + actionName: 'init', + summary: `Create an ${ExtractorConfig.FILENAME} config file`, + documentation: + `Use this command when setting up API Extractor for a new project. It writes an` + + ` ${ExtractorConfig.FILENAME} config file template with code comments that describe all the settings.` + + ` The file will be written in the current directory.`, + }); + } + + protected async onExecute(): Promise { + // override + const inputFilePath: string = path.resolve(__dirname, './schemas/api-extractor-template.json'); + const outputFilePath: string = path.resolve(ExtractorConfig.FILENAME); + + if (FileSystem.exists(outputFilePath)) { + console.log(colors.red('The output file already exists:')); + console.log('\n ' + outputFilePath + '\n'); + throw new Error('Unable to write output file'); + } + + console.log(colors.green('Writing file: ') + outputFilePath); + FileSystem.copyFile({ + sourcePath: inputFilePath, + destinationPath: outputFilePath, + }); + + console.log( + '\nThe recommended location for this file is in the project\'s "config" subfolder,\n' + + 'or else in the top-level folder with package.json.', + ); + } +} diff --git a/packages/api-extractor/src/cli/RunAction.ts b/packages/api-extractor/src/cli/RunAction.ts new file mode 100644 index 000000000..0736df743 --- /dev/null +++ b/packages/api-extractor/src/cli/RunAction.ts @@ -0,0 +1,156 @@ +/* eslint-disable n/prefer-global/process */ +/* eslint-disable no-restricted-globals */ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as os from 'node:os'; +import * as path from 'node:path'; +import { PackageJsonLookup, FileSystem, type IPackageJson, Path } from '@rushstack/node-core-library'; +import { + CommandLineAction, + type CommandLineStringParameter, + type CommandLineFlagParameter, +} from '@rushstack/ts-command-line'; +import colors from 'colors'; +import { Extractor, type ExtractorResult } from '../api/Extractor.js'; +import { ExtractorConfig, type IExtractorConfigPrepareOptions } from '../api/ExtractorConfig.js'; +import type { ApiExtractorCommandLine } from './ApiExtractorCommandLine.js'; + +export class RunAction extends CommandLineAction { + private readonly _configFileParameter: CommandLineStringParameter; + + private readonly _localParameter: CommandLineFlagParameter; + + private readonly _verboseParameter: CommandLineFlagParameter; + + private readonly _diagnosticsParameter: CommandLineFlagParameter; + + private readonly _typescriptCompilerFolder: CommandLineStringParameter; + + public constructor(_parser: ApiExtractorCommandLine) { + super({ + actionName: 'run', + summary: 'Invoke API Extractor on a project', + documentation: 'Invoke API Extractor on a project', + }); + + this._configFileParameter = this.defineStringParameter({ + parameterLongName: '--config', + parameterShortName: '-c', + argumentName: 'FILE', + description: `Use the specified ${ExtractorConfig.FILENAME} file path, rather than guessing its location`, + }); + + this._localParameter = this.defineFlagParameter({ + parameterLongName: '--local', + parameterShortName: '-l', + description: + 'Indicates that API Extractor is running as part of a local build,' + + " e.g. on a developer's machine. This disables certain validation that would" + + ' normally be performed for a ship/production build. For example, the *.api.md' + + ' report file is automatically copied in a local build.', + }); + + this._verboseParameter = this.defineFlagParameter({ + parameterLongName: '--verbose', + parameterShortName: '-v', + description: 'Show additional informational messages in the output.', + }); + + this._diagnosticsParameter = this.defineFlagParameter({ + parameterLongName: '--diagnostics', + description: + 'Show diagnostic messages used for troubleshooting problems with API Extractor.' + + ' This flag also enables the "--verbose" flag.', + }); + + this._typescriptCompilerFolder = this.defineStringParameter({ + parameterLongName: '--typescript-compiler-folder', + argumentName: 'PATH', + description: + 'API Extractor uses its own TypeScript compiler engine to analyze your project. If your project' + + ' is built with a significantly different TypeScript version, sometimes API Extractor may report compilation' + + ' errors due to differences in the system typings (e.g. lib.dom.d.ts). You can use the' + + ' "--typescriptCompilerFolder" option to specify the folder path where you installed the TypeScript package,' + + " and API Extractor's compiler will use those system typings instead.", + }); + } + + protected async onExecute(): Promise { + // override + const lookup: PackageJsonLookup = new PackageJsonLookup(); + let configFilename: string; + + let typescriptCompilerFolder: string | undefined = this._typescriptCompilerFolder.value; + if (typescriptCompilerFolder) { + typescriptCompilerFolder = path.normalize(typescriptCompilerFolder); + + if (FileSystem.exists(typescriptCompilerFolder)) { + typescriptCompilerFolder = lookup.tryGetPackageFolderFor(typescriptCompilerFolder); + const typescriptCompilerPackageJson: IPackageJson | undefined = typescriptCompilerFolder + ? lookup.tryLoadPackageJsonFor(typescriptCompilerFolder) + : undefined; + if (!typescriptCompilerPackageJson) { + throw new Error( + `The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a package.`, + ); + } else if (typescriptCompilerPackageJson.name !== 'typescript') { + throw new Error( + `The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a TypeScript` + + ' compiler package.', + ); + } + } else { + throw new Error( + `The path specified in the ${this._typescriptCompilerFolder.longName} parameter does not exist.`, + ); + } + } + + let extractorConfig: ExtractorConfig; + + if (this._configFileParameter.value) { + configFilename = path.normalize(this._configFileParameter.value); + if (!FileSystem.exists(configFilename)) { + throw new Error('Config file not found: ' + this._configFileParameter.value); + } + + extractorConfig = ExtractorConfig.loadFileAndPrepare(configFilename); + } else { + const prepareOptions: IExtractorConfigPrepareOptions | undefined = ExtractorConfig.tryLoadForFolder({ + startingFolder: '.', + }); + + if (!prepareOptions?.configObjectFullPath) { + throw new Error(`Unable to find an ${ExtractorConfig.FILENAME} file`); + } + + const configObjectShortPath: string = Path.formatConcisely({ + pathToConvert: prepareOptions.configObjectFullPath, + baseFolder: process.cwd(), + }); + console.log(`Using configuration from ${configObjectShortPath}`); + + extractorConfig = ExtractorConfig.prepare(prepareOptions); + } + + const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { + localBuild: this._localParameter.value, + showVerboseMessages: this._verboseParameter.value, + showDiagnostics: this._diagnosticsParameter.value, + typescriptCompilerFolder, + }); + + if (extractorResult.succeeded) { + console.log(os.EOL + 'API Extractor completed successfully'); + } else { + process.exitCode = 1; + + if (extractorResult.errorCount > 0) { + console.log(os.EOL + colors.red('API Extractor completed with errors')); + } else { + console.log(os.EOL + colors.yellow('API Extractor completed with warnings')); + } + } + } +} diff --git a/packages/api-extractor/src/collector/ApiItemMetadata.ts b/packages/api-extractor/src/collector/ApiItemMetadata.ts new file mode 100644 index 000000000..146836f2f --- /dev/null +++ b/packages/api-extractor/src/collector/ApiItemMetadata.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ReleaseTag } from '@discordjs/api-extractor-model'; +import type * as tsdoc from '@microsoft/tsdoc'; +import { VisitorState } from './VisitorState.js'; + +/** + * Constructor parameters for `ApiItemMetadata`. + */ +export interface IApiItemMetadataOptions { + declaredReleaseTag: ReleaseTag; + effectiveReleaseTag: ReleaseTag; + isEventProperty: boolean; + isOverride: boolean; + isPreapproved: boolean; + isSealed: boolean; + isVirtual: boolean; + releaseTagSameAsParent: boolean; +} + +/** + * Stores the Collector's additional analysis for an `AstDeclaration`. This object is assigned to + * `AstDeclaration.apiItemMetadata` but consumers must always obtain it by calling `Collector.fetchApiItemMetadata()`. + * + * @remarks + * Note that ancillary declarations share their `ApiItemMetadata` with the main declaration, + * whereas a separate `DeclarationMetadata` object is created for each declaration. + * + * Consider this example: + * ```ts + * export declare class A { + * get b(): string; + * set b(value: string); + * } + * export declare namespace A { } + * ``` + * + * In this example, there are two "symbols": `A` and `b` + * + * There are four "declarations": `A` class, `A` namespace, `b` getter, `b` setter + * + * There are three "API items": `A` class, `A` namespace, `b` property. The property getter is the main declaration + * for `b`, and the setter is the "ancillary" declaration. + */ +export class ApiItemMetadata { + /** + * This is the release tag that was explicitly specified in the original doc comment, if any. + */ + public readonly declaredReleaseTag: ReleaseTag; + + /** + * The "effective" release tag is a normalized value that is based on `declaredReleaseTag`, + * but may be inherited from a parent, or corrected if the declared value was somehow invalid. + * When actually trimming .d.ts files or generating docs, API Extractor uses the "effective" value + * instead of the "declared" value. + */ + public readonly effectiveReleaseTag: ReleaseTag; + + // If true, then it would be redundant to show this release tag + public readonly releaseTagSameAsParent: boolean; + + // NOTE: In the future, the Collector may infer or error-correct some of these states. + // Generators should rely on these instead of tsdocComment.modifierTagSet. + public readonly isEventProperty: boolean; + + public readonly isOverride: boolean; + + public readonly isSealed: boolean; + + public readonly isVirtual: boolean; + + public readonly isPreapproved: boolean; + + /** + * This is the TSDoc comment for the declaration. It may be modified (or constructed artificially) by + * the DocCommentEnhancer. + */ + public tsdocComment: tsdoc.DocComment | undefined; + + // Assigned by DocCommentEnhancer + public undocumented: boolean = true; + + public docCommentEnhancerVisitorState: VisitorState = VisitorState.Unvisited; + + public constructor(options: IApiItemMetadataOptions) { + this.declaredReleaseTag = options.declaredReleaseTag; + this.effectiveReleaseTag = options.effectiveReleaseTag; + this.releaseTagSameAsParent = options.releaseTagSameAsParent; + this.isEventProperty = options.isEventProperty; + this.isOverride = options.isOverride; + this.isSealed = options.isSealed; + this.isVirtual = options.isVirtual; + this.isPreapproved = options.isPreapproved; + } +} diff --git a/packages/api-extractor/src/collector/Collector.ts b/packages/api-extractor/src/collector/Collector.ts new file mode 100644 index 000000000..278b65edf --- /dev/null +++ b/packages/api-extractor/src/collector/Collector.ts @@ -0,0 +1,953 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ReleaseTag } from '@discordjs/api-extractor-model'; +import * as tsdoc from '@microsoft/tsdoc'; +import { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import { 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 { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; +import { AstReferenceResolver } from '../analyzer/AstReferenceResolver.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +import { AstSymbolTable } from '../analyzer/AstSymbolTable.js'; +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 { 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 { WorkingPackage } from './WorkingPackage.js'; + +/** + * Options for Collector constructor. + */ +export interface ICollectorOptions { + extractorConfig: ExtractorConfig; + + messageRouter: MessageRouter; + + /** + * Configuration for the TypeScript compiler. The most important options to set are: + * + * - target: ts.ScriptTarget.ES5 + * - module: ts.ModuleKind.CommonJS + * - moduleResolution: ts.ModuleResolutionKind.NodeJs + * - rootDir: inputFolder + */ + program: ts.Program; + + sourceMapper: SourceMapper; +} + +/** + * The `Collector` manages the overall data set that is used by `ApiModelGenerator`, + * `DtsRollupGenerator`, and `ApiReportGenerator`. Starting from the working package's entry point, + * the `Collector` collects all exported symbols, determines how to import any symbols they reference, + * assigns unique names, and sorts everything into a normalized alphabetical ordering. + */ +export class Collector { + public readonly program: ts.Program; + + public readonly typeChecker: ts.TypeChecker; + + public readonly globalVariableAnalyzer: IGlobalVariableAnalyzer; + + public readonly astSymbolTable: AstSymbolTable; + + public readonly astReferenceResolver: AstReferenceResolver; + + public readonly packageJsonLookup: PackageJsonLookup; + + public readonly messageRouter: MessageRouter; + + public readonly workingPackage: WorkingPackage; + + public readonly extractorConfig: ExtractorConfig; + + public readonly sourceMapper: SourceMapper; + + /** + * The `ExtractorConfig.bundledPackages` names in a set. + */ + public readonly bundledPackageNames: ReadonlySet; + + private readonly _program: ts.Program; + + private readonly _tsdocParser: tsdoc.TSDocParser; + + private _astEntryPoint: AstModule | undefined; + + private readonly _entities: CollectorEntity[] = []; + + private readonly _entitiesByAstEntity: Map = new Map(); + + private readonly _entitiesBySymbol: Map = new Map(); + + private readonly _starExportedExternalModulePaths: string[] = []; + + private readonly _dtsTypeReferenceDirectives: Set = new Set(); + + private readonly _dtsLibReferenceDirectives: Set = new Set(); + + // Used by getOverloadIndex() + private readonly _cachedOverloadIndexesByDeclaration: Map; + + public constructor(options: ICollectorOptions) { + this.packageJsonLookup = new PackageJsonLookup(); + + this._program = options.program; + this.extractorConfig = options.extractorConfig; + this.sourceMapper = options.sourceMapper; + + const entryPointSourceFile: ts.SourceFile | undefined = options.program.getSourceFile( + this.extractorConfig.mainEntryPointFilePath, + ); + + if (!entryPointSourceFile) { + throw new Error('Unable to load file: ' + this.extractorConfig.mainEntryPointFilePath); + } + + if (!this.extractorConfig.packageFolder || !this.extractorConfig.packageJson) { + throw new Error('Unable to find a package.json file for the project being analyzed'); + } + + this.workingPackage = new WorkingPackage({ + packageFolder: this.extractorConfig.packageFolder, + packageJson: this.extractorConfig.packageJson, + entryPointSourceFile, + }); + + this.messageRouter = options.messageRouter; + + this.program = options.program; + this.typeChecker = options.program.getTypeChecker(); + this.globalVariableAnalyzer = TypeScriptInternals.getGlobalVariableAnalyzer(this.program); + + this._tsdocParser = new tsdoc.TSDocParser(this.extractorConfig.tsdocConfiguration); + + this.bundledPackageNames = new Set(this.extractorConfig.bundledPackages); + + this.astSymbolTable = new AstSymbolTable( + this.program, + this.typeChecker, + this.packageJsonLookup, + this.bundledPackageNames, + this.messageRouter, + ); + this.astReferenceResolver = new AstReferenceResolver(this); + + this._cachedOverloadIndexesByDeclaration = new Map(); + } + + /** + * Returns a list of names (e.g. "example-library") that should appear in a reference like this: + * + * ``` + * /// + * ``` + */ + public get dtsTypeReferenceDirectives(): ReadonlySet { + return this._dtsTypeReferenceDirectives; + } + + /** + * A list of names (e.g. "runtime-library") that should appear in a reference like this: + * + * ``` + * /// + * ``` + */ + public get dtsLibReferenceDirectives(): ReadonlySet { + return this._dtsLibReferenceDirectives; + } + + public get entities(): readonly CollectorEntity[] { + return this._entities; + } + + /** + * A list of module specifiers (e.g. `"@rushstack/node-core-library/lib/FileSystem"`) that should be emitted + * as star exports (e.g. `export * from "@rushstack/node-core-library/lib/FileSystem"`). + */ + public get starExportedExternalModulePaths(): readonly string[] { + return this._starExportedExternalModulePaths; + } + + /** + * Perform the analysis. + */ + public analyze(): void { + if (this._astEntryPoint) { + throw new Error('DtsRollupGenerator.analyze() was already called'); + } + + // This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations) + // with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday + // compile errors that would result from a full compilation. + for (const diagnostic of this._program.getSemanticDiagnostics()) { + this.messageRouter.addCompilerDiagnostic(diagnostic); + } + + const sourceFiles: readonly ts.SourceFile[] = this.program.getSourceFiles(); + + if (this.messageRouter.showDiagnostics) { + this.messageRouter.logDiagnosticHeader('Root filenames'); + for (const fileName of this.program.getRootFileNames()) { + this.messageRouter.logDiagnostic(fileName); + } + + this.messageRouter.logDiagnosticFooter(); + + this.messageRouter.logDiagnosticHeader('Files analyzed by compiler'); + for (const sourceFile of sourceFiles) { + this.messageRouter.logDiagnostic(sourceFile.fileName); + } + + this.messageRouter.logDiagnosticFooter(); + } + + // We can throw this error earlier in CompilerState.ts, but intentionally wait until after we've logged the + // associated diagnostic message above to make debugging easier for developers. + // Typically there will be many such files -- to avoid too much noise, only report the first one. + const badSourceFile: ts.SourceFile | undefined = sourceFiles.find( + ({ fileName }) => !ExtractorConfig.hasDtsFileExtension(fileName), + ); + if (badSourceFile) { + this.messageRouter.addAnalyzerIssueForPosition( + ExtractorMessageId.WrongInputFileType, + 'Incorrect file type; API Extractor expects to analyze compiler outputs with the .d.ts file extension. ' + + 'Troubleshooting tips: https://api-extractor.com/link/dts-error', + badSourceFile, + 0, + ); + } + + // Build the entry point + const entryPointSourceFile: ts.SourceFile = this.workingPackage.entryPointSourceFile; + + const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile); + this._astEntryPoint = astEntryPoint; + + const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile( + entryPointSourceFile, + this, + ); + + if (packageDocCommentTextRange) { + const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange( + entryPointSourceFile.text, + packageDocCommentTextRange.pos, + packageDocCommentTextRange.end, + ); + + this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range); + + this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile); + + this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment; + } + + const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint); + + // 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); + } + + // Recursively create the remaining CollectorEntities after the top-level entities + // have been processed. + const alreadySeenAstEntities: Set = new Set(); + for (const astEntity of processedAstEntities) { + this._recursivelyCreateEntities(astEntity, alreadySeenAstEntities); + if (astEntity instanceof AstSymbol) { + this.fetchSymbolMetadata(astEntity); + } + } + + this._makeUniqueNames(); + + for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) { + if (starExportedExternalModule.externalModulePath !== undefined) { + this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath); + } + } + + 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 + this._starExportedExternalModulePaths.sort(); + } + + /** + * For a given ts.Identifier that is part of an AstSymbol that we analyzed, return the CollectorEntity that + * it refers to. Returns undefined if it doesn't refer to anything interesting. + * + * @remarks + * Throws an Error if the ts.Identifier is not part of node tree that was analyzed. + */ + public tryGetEntityForNode(identifier: ts.Identifier | ts.ImportTypeNode): CollectorEntity | undefined { + const astEntity: AstEntity | undefined = this.astSymbolTable.tryGetEntityForNode(identifier); + if (astEntity) { + return this._entitiesByAstEntity.get(astEntity); + } + + return undefined; + } + + /** + * For a given analyzed ts.Symbol, return the CollectorEntity that it refers to. Returns undefined if it + * doesn't refer to anything interesting. + */ + public tryGetEntityForSymbol(symbol: ts.Symbol): CollectorEntity | undefined { + return this._entitiesBySymbol.get(symbol); + } + + /** + * Returns the associated `CollectorEntity` for the given `astEntity`, if one was created during analysis. + */ + public tryGetCollectorEntity(astEntity: AstEntity): CollectorEntity | undefined { + return this._entitiesByAstEntity.get(astEntity); + } + + public fetchSymbolMetadata(astSymbol: AstSymbol): SymbolMetadata { + if (astSymbol.symbolMetadata === undefined) { + this._fetchSymbolMetadata(astSymbol); + } + + return astSymbol.symbolMetadata as SymbolMetadata; + } + + public fetchDeclarationMetadata(astDeclaration: AstDeclaration): DeclarationMetadata { + if (astDeclaration.declarationMetadata === undefined) { + // Fetching the SymbolMetadata always constructs the DeclarationMetadata + this._fetchSymbolMetadata(astDeclaration.astSymbol); + } + + return astDeclaration.declarationMetadata as DeclarationMetadata; + } + + public fetchApiItemMetadata(astDeclaration: AstDeclaration): ApiItemMetadata { + if (astDeclaration.apiItemMetadata === undefined) { + // Fetching the SymbolMetadata always constructs the ApiItemMetadata + this._fetchSymbolMetadata(astDeclaration.astSymbol); + } + + return astDeclaration.apiItemMetadata as ApiItemMetadata; + } + + public tryFetchMetadataForAstEntity(astEntity: AstEntity): SymbolMetadata | undefined { + if (astEntity instanceof AstSymbol) { + return this.fetchSymbolMetadata(astEntity); + } + + if (astEntity instanceof AstImport && astEntity.astSymbol) { + return this.fetchSymbolMetadata(astEntity.astSymbol); + } + + return undefined; + } + + public isAncillaryDeclaration(astDeclaration: AstDeclaration): boolean { + const declarationMetadata: DeclarationMetadata = this.fetchDeclarationMetadata(astDeclaration); + return declarationMetadata.isAncillary; + } + + public getNonAncillaryDeclarations(astSymbol: AstSymbol): readonly AstDeclaration[] { + const result: AstDeclaration[] = []; + for (const astDeclaration of astSymbol.astDeclarations) { + const declarationMetadata: DeclarationMetadata = this.fetchDeclarationMetadata(astDeclaration); + if (!declarationMetadata.isAncillary) { + result.push(astDeclaration); + } + } + + return result; + } + + /** + * Removes the leading underscore, for example: "_Example" --\> "example*Example*_" + * + * @remarks + * This causes internal definitions to sort alphabetically case-insensitive, then case-sensitive, and + * initially ignoring the underscore prefix, while still deterministically comparing it. + * The star is used as a delimiter because it is not a legal identifier character. + */ + public static getSortKeyIgnoringUnderscore(identifier: string | undefined): string { + if (!identifier) return ''; + + let parts: string[]; + + if (identifier.startsWith('_')) { + const withoutUnderscore: string = identifier.slice(1); + parts = [withoutUnderscore.toLowerCase(), '*', withoutUnderscore, '*', '_']; + } else { + parts = [identifier.toLowerCase(), '*', identifier]; + } + + return parts.join(''); + } + + /** + * For function-like signatures, this returns the TSDoc "overload index" which can be used to identify + * a specific overload. + */ + public getOverloadIndex(astDeclaration: AstDeclaration): number { + const allDeclarations: readonly AstDeclaration[] = astDeclaration.astSymbol.astDeclarations; + if (allDeclarations.length === 1) { + return 1; // trivial case + } + + let overloadIndex: number | undefined = this._cachedOverloadIndexesByDeclaration.get(astDeclaration); + + if (overloadIndex === undefined) { + // TSDoc index selectors are positive integers counting from 1 + let nextIndex = 1; + for (const other of allDeclarations) { + // Filter out other declarations that are not overloads. For example, an overloaded function can also + // be a namespace. + if (other.declaration.kind === astDeclaration.declaration.kind) { + this._cachedOverloadIndexesByDeclaration.set(other, nextIndex); + ++nextIndex; + } + } + + overloadIndex = this._cachedOverloadIndexesByDeclaration.get(astDeclaration); + } + + if (overloadIndex === undefined) { + // This should never happen + throw new InternalError('Error calculating overload index for declaration'); + } + + return overloadIndex; + } + + private _createCollectorEntity(astEntity: AstEntity, exportName?: string, parent?: CollectorEntity): CollectorEntity { + let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity); + + if (!entity) { + entity = new CollectorEntity(astEntity); + + this._entitiesByAstEntity.set(astEntity, entity); + if (astEntity instanceof AstSymbol) { + this._entitiesBySymbol.set(astEntity.followedSymbol, entity); + } else if (astEntity instanceof AstNamespaceImport) { + this._entitiesBySymbol.set(astEntity.symbol, entity); + } + + this._entities.push(entity); + this._collectReferenceDirectives(astEntity); + } + + if (exportName) { + if (parent) { + entity.addLocalExportName(exportName, parent); + } else { + entity.addExportName(exportName); + } + } + + return entity; + } + + private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set): void { + if (alreadySeenAstEntities.has(astEntity)) return; + alreadySeenAstEntities.add(astEntity); + + if (astEntity instanceof AstSymbol) { + astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + for (const referencedAstEntity of astDeclaration.referencedAstEntities) { + if (referencedAstEntity instanceof AstSymbol) { + // We only create collector entities for root-level symbols. For example, if a symbol is + // 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); + } + } else { + this._createCollectorEntity(referencedAstEntity); + } + + this._recursivelyCreateEntities(referencedAstEntity, alreadySeenAstEntities); + } + }); + } + + if (astEntity instanceof AstNamespaceImport) { + const astModuleExportInfo: AstModuleExportInfo = 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. + throw new InternalError( + `Failed to get CollectorEntity for AstNamespaceImport with namespace name "${astEntity.namespaceName}"`, + ); + } + + 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); + } + } + } + + /** + * Ensures a unique name for each item in the package typings file. + */ + private _makeUniqueNames(): void { + // The following examples illustrate the nameForEmit heuristics: + // + // Example 1: + // class X { } <--- nameForEmit should be "A" to simplify things and reduce possibility of conflicts + // export { X as A }; + // + // Example 2: + // class X { } <--- nameForEmit should be "X" because choosing A or B would be nondeterministic + // export { X as A }; + // export { X as B }; + // + // Example 3: + // class X { } <--- nameForEmit should be "X_1" because Y has a stronger claim to the name + // export { X as A }; + // export { X as B }; + // class Y { } <--- nameForEmit should be "X" + // export { Y as X }; + + // Set of names that should NOT be used when generating a unique nameForEmit + const usedNames: Set = new Set(); + + // First collect the names of explicit package exports, and perform a sanity check. + for (const entity of this._entities) { + for (const exportName of entity.exportNames) { + if (usedNames.has(exportName)) { + // This should be impossible + throw new InternalError(`A package cannot have two exports with the name "${exportName}"`); + } + + usedNames.add(exportName); + } + } + + // Ensure that each entity has a unique nameForEmit + for (const entity of this._entities) { + // What name would we ideally want to emit it as? + let idealNameForEmit: string; + + // If this entity is exported exactly once, then we prefer the exported name + if (entity.singleExportName !== undefined && entity.singleExportName !== ts.InternalSymbolName.Default) { + idealNameForEmit = entity.singleExportName; + } else { + // otherwise use the local name + idealNameForEmit = entity.astEntity.localName; + } + + if (idealNameForEmit.includes('.')) { + // For an ImportType with a namespace chain, only the top namespace is imported. + idealNameForEmit = idealNameForEmit.split('.')[0]!; + } + + // If the idealNameForEmit happens to be the same as one of the exports, then we're safe to use that... + if ( + entity.exportNames.has(idealNameForEmit) && // ...except that if it conflicts with a global name, then the global name wins + !this.globalVariableAnalyzer.hasGlobalName(idealNameForEmit) && // ...also avoid "default" which can interfere with "export { default } from 'some-module;'" + idealNameForEmit !== 'default' + ) { + entity.nameForEmit = idealNameForEmit; + continue; + } + + // Generate a unique name based on idealNameForEmit + let suffix = 1; + let nameForEmit: string = idealNameForEmit; + + // Choose a name that doesn't conflict with usedNames or a global name + while ( + nameForEmit === 'default' || + usedNames.has(nameForEmit) || + this.globalVariableAnalyzer.hasGlobalName(nameForEmit) + ) { + nameForEmit = `${idealNameForEmit}_${++suffix}`; + } + + entity.nameForEmit = nameForEmit; + usedNames.add(nameForEmit); + } + } + + private _fetchSymbolMetadata(astSymbol: AstSymbol): void { + if (astSymbol.symbolMetadata) { + return; + } + + // When we solve an astSymbol, then we always also solve all of its parents and all of its declarations. + // The parent is solved first. + if (astSymbol.parentAstSymbol && astSymbol.parentAstSymbol.symbolMetadata === undefined) { + this._fetchSymbolMetadata(astSymbol.parentAstSymbol); + } + + // Construct the DeclarationMetadata objects, and detect any ancillary declarations + this._calculateDeclarationMetadataForDeclarations(astSymbol); + + // Calculate the ApiItemMetadata objects + for (const astDeclaration of astSymbol.astDeclarations) { + this._calculateApiItemMetadata(astDeclaration); + } + + // The most public effectiveReleaseTag for all declarations + let maxEffectiveReleaseTag: ReleaseTag = ReleaseTag.None; + + for (const astDeclaration of astSymbol.astDeclarations) { + // We know we solved this above + const apiItemMetadata: ApiItemMetadata = astDeclaration.apiItemMetadata as ApiItemMetadata; + + const effectiveReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + + if (effectiveReleaseTag > maxEffectiveReleaseTag) { + maxEffectiveReleaseTag = effectiveReleaseTag; + } + } + + // Update this last when we're sure no exceptions were thrown + astSymbol.symbolMetadata = new SymbolMetadata({ + maxEffectiveReleaseTag, + }); + } + + private _calculateDeclarationMetadataForDeclarations(astSymbol: AstSymbol): void { + // Initialize DeclarationMetadata for each declaration + for (const astDeclaration of astSymbol.astDeclarations) { + if (astDeclaration.declarationMetadata) { + throw new InternalError('AstDeclaration.declarationMetadata is not expected to have been initialized yet'); + } + + const metadata: InternalDeclarationMetadata = new InternalDeclarationMetadata(); + metadata.tsdocParserContext = this._parseTsdocForAstDeclaration(astDeclaration); + + astDeclaration.declarationMetadata = metadata; + } + + // Detect ancillary declarations + for (const astDeclaration of astSymbol.astDeclarations) { + // For a getter/setter pair, make the setter ancillary to the getter + if (astDeclaration.declaration.kind === ts.SyntaxKind.SetAccessor) { + let foundGetter = false; + for (const getterAstDeclaration of astDeclaration.astSymbol.astDeclarations) { + if (getterAstDeclaration.declaration.kind === ts.SyntaxKind.GetAccessor) { + // Associate it with the getter + this._addAncillaryDeclaration(getterAstDeclaration, astDeclaration); + + foundGetter = true; + } + } + + if (!foundGetter) { + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.MissingGetter, + `The property "${astDeclaration.astSymbol.localName}" has a setter but no getter.`, + astDeclaration, + ); + } + } + } + } + + private _addAncillaryDeclaration(mainAstDeclaration: AstDeclaration, ancillaryAstDeclaration: AstDeclaration): void { + const mainMetadata: InternalDeclarationMetadata = + mainAstDeclaration.declarationMetadata as InternalDeclarationMetadata; + const ancillaryMetadata: InternalDeclarationMetadata = + ancillaryAstDeclaration.declarationMetadata as InternalDeclarationMetadata; + + if (mainMetadata.ancillaryDeclarations.includes(ancillaryAstDeclaration)) { + return; // already added + } + + if (mainAstDeclaration.astSymbol !== ancillaryAstDeclaration.astSymbol) { + throw new InternalError( + 'Invalid call to _addAncillaryDeclaration() because declarations do not belong to the same symbol', + ); + } + + if (mainMetadata.isAncillary) { + throw new InternalError('Invalid call to _addAncillaryDeclaration() because the target is ancillary itself'); + } + + if (ancillaryMetadata.isAncillary) { + throw new InternalError( + 'Invalid call to _addAncillaryDeclaration() because source is already ancillary to another declaration', + ); + } + + if (mainAstDeclaration.apiItemMetadata || ancillaryAstDeclaration.apiItemMetadata) { + throw new InternalError( + 'Invalid call to _addAncillaryDeclaration() because the API item metadata has already been constructed', + ); + } + + ancillaryMetadata.isAncillary = true; + mainMetadata.ancillaryDeclarations.push(ancillaryAstDeclaration); + } + + private _calculateApiItemMetadata(astDeclaration: AstDeclaration): void { + const declarationMetadata: InternalDeclarationMetadata = + astDeclaration.declarationMetadata as InternalDeclarationMetadata; + if (declarationMetadata.isAncillary) { + if (astDeclaration.declaration.kind === ts.SyntaxKind.SetAccessor && declarationMetadata.tsdocParserContext) { + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.SetterWithDocs, + `The doc comment for the property "${astDeclaration.astSymbol.localName}"` + + ` must appear on the getter, not the setter.`, + astDeclaration, + ); + } + + // We never calculate ApiItemMetadata for an ancillary declaration; instead, it is assigned when + // the main declaration is processed. + return; + } + + const options: IApiItemMetadataOptions = { + declaredReleaseTag: ReleaseTag.None, + effectiveReleaseTag: ReleaseTag.None, + isEventProperty: false, + isOverride: false, + isSealed: false, + isVirtual: false, + isPreapproved: false, + releaseTagSameAsParent: false, + }; + + const parserContext: tsdoc.ParserContext | undefined = declarationMetadata.tsdocParserContext; + if (parserContext) { + const modifierTagSet: tsdoc.StandardModifierTagSet = parserContext.docComment.modifierTagSet; + + let declaredReleaseTag: ReleaseTag = ReleaseTag.None; + let extraReleaseTags = false; + + if (modifierTagSet.isPublic()) { + declaredReleaseTag = ReleaseTag.Public; + } + + if (modifierTagSet.isBeta()) { + if (declaredReleaseTag === ReleaseTag.None) { + declaredReleaseTag = ReleaseTag.Beta; + } else { + extraReleaseTags = true; + } + } + + if (modifierTagSet.isAlpha()) { + if (declaredReleaseTag === ReleaseTag.None) { + declaredReleaseTag = ReleaseTag.Alpha; + } else { + extraReleaseTags = true; + } + } + + if (modifierTagSet.isInternal()) { + if (declaredReleaseTag === ReleaseTag.None) { + declaredReleaseTag = ReleaseTag.Internal; + } else { + extraReleaseTags = true; + } + } + + if (extraReleaseTags && !astDeclaration.astSymbol.isExternal) { + // for now, don't report errors for external code + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.ExtraReleaseTag, + 'The doc comment should not contain more than one release tag', + astDeclaration, + ); + } + + options.declaredReleaseTag = declaredReleaseTag; + + options.isEventProperty = modifierTagSet.isEventProperty(); + options.isOverride = modifierTagSet.isOverride(); + options.isSealed = modifierTagSet.isSealed(); + options.isVirtual = modifierTagSet.isVirtual(); + const preapprovedTag: tsdoc.TSDocTagDefinition | undefined = + this.extractorConfig.tsdocConfiguration.tryGetTagDefinition('@preapproved'); + + if (preapprovedTag && modifierTagSet.hasTag(preapprovedTag)) { + // This feature only makes sense for potentially big declarations. + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.ModuleDeclaration: + if (declaredReleaseTag === ReleaseTag.Internal) { + options.isPreapproved = true; + } else { + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.PreapprovedBadReleaseTag, + `The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"` + + ` without an @internal release tag`, + astDeclaration, + ); + } + + break; + default: + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.PreapprovedUnsupportedType, + `The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"` + + ` because it is not a supported declaration type`, + astDeclaration, + ); + break; + } + } + } + + // This needs to be set regardless of whether or not a parserContext exists + if (astDeclaration.parent) { + const parentApiItemMetadata: ApiItemMetadata = this.fetchApiItemMetadata(astDeclaration.parent); + options.effectiveReleaseTag = + options.declaredReleaseTag === ReleaseTag.None + ? parentApiItemMetadata.effectiveReleaseTag + : options.declaredReleaseTag; + + options.releaseTagSameAsParent = parentApiItemMetadata.effectiveReleaseTag === options.effectiveReleaseTag; + } else { + options.effectiveReleaseTag = options.declaredReleaseTag; + } + + if (options.effectiveReleaseTag === ReleaseTag.None) { + if (!astDeclaration.astSymbol.isExternal) { + // for now, don't report errors for external code + // Don't report missing release tags for forgotten exports (unless we're including forgotten exports + // in either the API report or doc model). + const astSymbol: AstSymbol = astDeclaration.astSymbol; + const entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astSymbol.rootAstSymbol); + if ( + entity && + (entity.consumable || + this.extractorConfig.apiReportIncludeForgottenExports || + this.extractorConfig.docModelIncludeForgottenExports) && // We also don't report errors for the default export of an entry point, since its doc comment + // isn't easy to obtain from the .d.ts file + astSymbol.rootAstSymbol.localName !== '_default' + ) { + this.messageRouter.addAnalyzerIssue( + ExtractorMessageId.MissingReleaseTag, + `"${entity.astEntity.localName}" is part of the package's API, but it is missing ` + + `a release tag (@alpha, @beta, @public, or @internal)`, + astSymbol, + ); + } + } + + options.effectiveReleaseTag = ReleaseTag.Public; + } + + const apiItemMetadata: ApiItemMetadata = new ApiItemMetadata(options); + if (parserContext) { + apiItemMetadata.tsdocComment = parserContext.docComment; + } + + astDeclaration.apiItemMetadata = apiItemMetadata; + + // Lastly, share the result with any ancillary declarations + for (const ancillaryDeclaration of declarationMetadata.ancillaryDeclarations) { + ancillaryDeclaration.apiItemMetadata = apiItemMetadata; + } + } + + private _parseTsdocForAstDeclaration(astDeclaration: AstDeclaration): tsdoc.ParserContext | undefined { + const declaration: ts.Declaration = astDeclaration.declaration; + let nodeForComment: ts.Node = declaration; + + if (ts.isVariableDeclaration(declaration)) { + // Variable declarations are special because they can be combined into a list. For example: + // + // /** A */ export /** B */ const /** C */ x = 1, /** D **/ [ /** E */ y, z] = [3, 4]; + // + // The compiler will only emit comments A and C in the .d.ts file, so in general there isn't a well-defined + // way to document these parts. API Extractor requires you to break them into separate exports like this: + // + // /** A */ export const x = 1; + // + // But _getReleaseTagForDeclaration() still receives a node corresponding to "x", so we need to walk upwards + // and find the containing statement in order for getJSDocCommentRanges() to read the comment that we expect. + const statement: ts.VariableStatement | undefined = TypeScriptHelpers.findFirstParent( + declaration, + ts.SyntaxKind.VariableStatement, + ) as ts.VariableStatement | undefined; + if ( + statement !== undefined && // For a compound declaration, fall back to looking for C instead of A + statement.declarationList.declarations.length === 1 + ) { + nodeForComment = statement; + } + } + + const sourceFileText: string = declaration.getSourceFile().text; + const ranges: ts.CommentRange[] = TypeScriptInternals.getJSDocCommentRanges(nodeForComment, sourceFileText) ?? []; + + if (ranges.length === 0) { + return undefined; + } + + // We use the JSDoc comment block that is closest to the definition, i.e. + // the last one preceding it + const range: ts.TextRange = ranges[ranges.length - 1]!; + + const tsdocTextRange: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(sourceFileText, range.pos, range.end); + + const parserContext: tsdoc.ParserContext = this._tsdocParser.parseRange(tsdocTextRange); + + this.messageRouter.addTsdocMessages(parserContext, declaration.getSourceFile(), astDeclaration); + + // We delete the @privateRemarks block as early as possible, to ensure that it never leaks through + // into one of the output files. + parserContext.docComment.privateRemarks = undefined; + + return parserContext; + } + + private _collectReferenceDirectives(astEntity: AstEntity): void { + if (astEntity instanceof AstSymbol) { + const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) => + astDeclaration.declaration.getSourceFile(), + ); + this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + return; + } + + if (astEntity instanceof AstNamespaceImport) { + const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile]; + this._collectReferenceDirectivesFromSourceFiles(sourceFiles); + } + } + + private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void { + const seenFilenames: Set = new Set(); + + for (const sourceFile of sourceFiles) { + if (sourceFile?.fileName && !seenFilenames.has(sourceFile.fileName)) { + seenFilenames.add(sourceFile.fileName); + + for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) { + const name: string = sourceFile.text.slice(typeReferenceDirective.pos, typeReferenceDirective.end); + this._dtsTypeReferenceDirectives.add(name); + } + + for (const libReferenceDirective of sourceFile.libReferenceDirectives) { + const name: string = sourceFile.text.slice(libReferenceDirective.pos, libReferenceDirective.end); + this._dtsLibReferenceDirectives.add(name); + } + } + } + } +} diff --git a/packages/api-extractor/src/collector/CollectorEntity.ts b/packages/api-extractor/src/collector/CollectorEntity.ts new file mode 100644 index 000000000..0e5580c24 --- /dev/null +++ b/packages/api-extractor/src/collector/CollectorEntity.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Sort } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import type { AstEntity } from '../analyzer/AstEntity.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +import { Collector } from './Collector.js'; + +/** + * This is a data structure used by the Collector to track an AstEntity that may be emitted in the *.d.ts file. + * + * @remarks + * The additional contextual state beyond AstSymbol is: + * - Whether it's an export of this entry point or not + * - The nameForEmit, which may get renamed by DtsRollupGenerator._makeUniqueNames() + * - The export name (or names, if the same symbol is exported multiple times) + */ +export class CollectorEntity { + /** + * The AstEntity that this entry represents. + */ + public readonly astEntity: AstEntity; + + private _exportNames: Set = new Set(); + + private _exportNamesSorted: boolean = false; + + private _singleExportName: string | undefined = undefined; + + private _localExportNamesByParent: Map> = new Map(); + + private _nameForEmit: string | undefined = undefined; + + private _sortKey: string | undefined = undefined; + + public constructor(astEntity: AstEntity) { + this.astEntity = astEntity; + } + + /** + * The declaration name that will be emitted in the .d.ts rollup, .api.md, and .api.json files. Generated by + * `Collector._makeUniqueNames`. Be aware that the declaration may be renamed to avoid conflicts with (1) + * global names (e.g. `Promise`) and (2) if local, other local names across different files. + */ + public get nameForEmit(): string | undefined { + return this._nameForEmit; + } + + public set nameForEmit(value: string | undefined) { + this._nameForEmit = value; + this._sortKey = undefined; // invalidate the cached value + } + + /** + * The list of export names if this symbol is exported from the entry point. + * + * @remarks + * Note that a given symbol may be exported more than once: + * ``` + * class X { } + * export { X } + * export { X as Y } + * ``` + */ + public get exportNames(): ReadonlySet { + if (!this._exportNamesSorted) { + Sort.sortSet(this._exportNames); + this._exportNamesSorted = true; + } + + return this._exportNames; + } + + /** + * If exportNames contains only one string, then singleExportName is that string. + * In all other cases, it is undefined. + */ + public get singleExportName(): string | undefined { + return this._singleExportName; + } + + /** + * This is true if exportNames contains only one string, and the declaration can be exported using the inline syntax + * such as "export class X \{ \}" instead of "export \{ X \}". + */ + public get shouldInlineExport(): boolean { + // We don't inline an AstImport + return ( + this.astEntity instanceof AstSymbol && // We don't inline a symbol with more than one exported name + this._singleExportName !== undefined && + this._singleExportName !== ts.InternalSymbolName.Default && // We can't inline a symbol whose emitted name is different from the export name + (this._nameForEmit === undefined || this._nameForEmit === this._singleExportName) + ); + } + + /** + * Indicates that this entity is exported from the package entry point. Compare to `CollectorEntity.exported`. + */ + public get exportedFromEntryPoint(): boolean { + return this.exportNames.size > 0; + } + + /** + * Indicates that this entity is exported from its parent module (i.e. either the package entry point or + * a local namespace). Compare to `CollectorEntity.consumable`. + * + * @remarks + * In the example below: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * ``` + * + * Namespace `calculator` is neither exported nor consumable, function `add` is exported (from `calculator`) + * but not consumable. + */ + public get exported(): boolean { + // Exported from top-level? + if (this.exportedFromEntryPoint) return true; + + // Exported from parent? + for (const localExportNames of this._localExportNamesByParent.values()) { + if (localExportNames.size > 0) { + return true; + } + } + + return false; + } + + /** + * Indicates that it is possible for a consumer of the API to "consume" this entity, either by importing + * it directly or via a namespace. If an entity is not consumable, then API Extractor will report an + * `ae-forgotten-export` warning. Compare to `CollectorEntity.exported`. + * + * @remarks + * An API item is consumable if: + * + * 1. It is exported from the top-level entry point OR + * 2. It is exported from a consumable parent entity. + * + * For an example of #2, consider how `AstNamespaceImport` entities are processed. A generated rollup.d.ts + * might look like this: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * export { calculator } + * ``` + * + * In this example, `add` is exported via the consumable `calculator` namespace. + */ + public get consumable(): boolean { + // Exported from top-level? + if (this.exportedFromEntryPoint) return true; + + // Exported from consumable parent? + for (const [parent, localExportNames] of this._localExportNamesByParent) { + if (localExportNames.size > 0 && parent.consumable) { + return true; + } + } + + return false; + } + + /** + * Return the first consumable parent that exports this entity. If there is none, returns + * `undefined`. + */ + public getFirstExportingConsumableParent(): CollectorEntity | undefined { + for (const [parent, localExportNames] of this._localExportNamesByParent) { + if (parent.consumable && localExportNames.size > 0) { + return parent; + } + } + + return undefined; + } + + /** + * Adds a new export name to the entity. + */ + public addExportName(exportName: string): void { + if (!this._exportNames.has(exportName)) { + this._exportNamesSorted = false; + this._exportNames.add(exportName); + + if (this._exportNames.size === 1) { + this._singleExportName = exportName; + } else { + this._singleExportName = undefined; + } + } + } + + /** + * Adds a new local export name to the entity. + * + * @remarks + * In the example below: + * + * ```ts + * declare function add(): void; + * declare namespace calculator { + * export { + * add + * } + * } + * ``` + * + * `add` is the local export name for the `CollectorEntity` for `add`. + */ + public addLocalExportName(localExportName: string, parent: CollectorEntity): void { + const localExportNames: Set = this._localExportNamesByParent.get(parent) ?? new Set(); + localExportNames.add(localExportName); + + this._localExportNamesByParent.set(parent, localExportNames); + } + + /** + * A sorting key used by DtsRollupGenerator._makeUniqueNames() + */ + public getSortKey(): string { + if (!this._sortKey) { + this._sortKey = Collector.getSortKeyIgnoringUnderscore(this.nameForEmit ?? this.astEntity.localName); + } + + return this._sortKey; + } +} diff --git a/packages/api-extractor/src/collector/DeclarationMetadata.ts b/packages/api-extractor/src/collector/DeclarationMetadata.ts new file mode 100644 index 000000000..a1c1b72ce --- /dev/null +++ b/packages/api-extractor/src/collector/DeclarationMetadata.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; + +/** + * Stores the Collector's additional analysis for a specific `AstDeclaration` signature. This object is assigned to + * `AstDeclaration.declarationMetadata` but consumers must always obtain it by calling + * `Collector.fetchDeclarationMetadata()`. + * + * Note that ancillary declarations share their `ApiItemMetadata` with the main declaration, + * whereas a separate `DeclarationMetadata` object is created for each declaration. + */ +export abstract class DeclarationMetadata { + /** + * The ParserContext from when the TSDoc comment was parsed from the source code. + * If the source code did not contain a doc comment, then this will be undefined. + * + * Note that if an ancillary declaration has a doc comment, it is tracked here, whereas + * `ApiItemMetadata.tsdocComment` corresponds to documentation for the main declaration. + */ + public abstract readonly tsdocParserContext: tsdoc.ParserContext | undefined; + + /** + * If true, then this declaration is treated as part of another declaration. + */ + public abstract readonly isAncillary: boolean; + + /** + * A list of other declarations that are treated as being part of this declaration. For example, a property + * getter/setter pair will be treated as a single API item, with the setter being treated as ancillary to the getter. + * + * If the `ancillaryDeclarations` array is non-empty, then `isAncillary` will be false for this declaration, + * and `isAncillary` will be true for all the array items. + */ + public abstract readonly ancillaryDeclarations: readonly AstDeclaration[]; +} + +/** + * Used internally by the `Collector` to build up `DeclarationMetadata`. + */ +export class InternalDeclarationMetadata extends DeclarationMetadata { + public tsdocParserContext: tsdoc.ParserContext | undefined = undefined; + + public isAncillary: boolean = false; + + public ancillaryDeclarations: AstDeclaration[] = []; +} diff --git a/packages/api-extractor/src/collector/MessageRouter.ts b/packages/api-extractor/src/collector/MessageRouter.ts new file mode 100644 index 000000000..93e4adfb0 --- /dev/null +++ b/packages/api-extractor/src/collector/MessageRouter.ts @@ -0,0 +1,648 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import { Sort, InternalError } from '@rushstack/node-core-library'; +import colors from 'colors'; +import * as ts from 'typescript'; +import { AstDeclaration } from '../analyzer/AstDeclaration.js'; +import type { AstSymbol } from '../analyzer/AstSymbol.js'; +import { ConsoleMessageId } from '../api/ConsoleMessageId.js'; +import { ExtractorLogLevel } from '../api/ExtractorLogLevel.js'; +import { + ExtractorMessage, + ExtractorMessageCategory, + type IExtractorMessageOptions, + type IExtractorMessageProperties, +} from '../api/ExtractorMessage.js'; +import { type ExtractorMessageId, allExtractorMessageIds } from '../api/ExtractorMessageId.js'; +import type { IExtractorMessagesConfig, IConfigMessageReportingRule } from '../api/IConfigFile.js'; +import type { ISourceLocation, SourceMapper } from './SourceMapper.js'; + +interface IReportingRule { + addToApiReportFile: boolean; + logLevel: ExtractorLogLevel; +} + +export interface IMessageRouterOptions { + messageCallback: ((message: ExtractorMessage) => void) | undefined; + messagesConfig: IExtractorMessagesConfig; + showDiagnostics: boolean; + showVerboseMessages: boolean; + sourceMapper: SourceMapper; + tsdocConfiguration: tsdoc.TSDocConfiguration; + workingPackageFolder: string | undefined; +} + +export interface IBuildJsonDumpObjectOptions { + /** + * {@link MessageRouter.buildJsonDumpObject} will omit any objects keys with these names. + */ + keyNamesToOmit?: string[]; +} + +export class MessageRouter { + public static readonly DIAGNOSTICS_LINE: string = '============================================================'; + + private readonly _workingPackageFolder: string | undefined; + + private readonly _messageCallback: ((message: ExtractorMessage) => void) | undefined; + + // All messages + private readonly _messages: ExtractorMessage[]; + + // For each AstDeclaration, the messages associated with it. This is used when addToApiReportFile=true + private readonly _associatedMessagesForAstDeclaration: Map; + + private readonly _sourceMapper: SourceMapper; + + private readonly _tsdocConfiguration: tsdoc.TSDocConfiguration; + + // Normalized representation of the routing rules from api-extractor.json + private _reportingRuleByMessageId: Map = new Map(); + + private _compilerDefaultRule: IReportingRule = { + logLevel: ExtractorLogLevel.None, + addToApiReportFile: false, + }; + + private _extractorDefaultRule: IReportingRule = { + logLevel: ExtractorLogLevel.None, + addToApiReportFile: false, + }; + + private _tsdocDefaultRule: IReportingRule = { logLevel: ExtractorLogLevel.None, addToApiReportFile: false }; + + public errorCount: number = 0; + + public warningCount: number = 0; + + /** + * See {@link IExtractorInvokeOptions.showVerboseMessages} + */ + public readonly showVerboseMessages: boolean; + + /** + * See {@link IExtractorInvokeOptions.showDiagnostics} + */ + public readonly showDiagnostics: boolean; + + public constructor(options: IMessageRouterOptions) { + this._workingPackageFolder = options.workingPackageFolder; + this._messageCallback = options.messageCallback; + + this._messages = []; + this._associatedMessagesForAstDeclaration = new Map(); + this._sourceMapper = options.sourceMapper; + this._tsdocConfiguration = options.tsdocConfiguration; + + // showDiagnostics implies showVerboseMessages + this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics; + this.showDiagnostics = options.showDiagnostics; + + this._applyMessagesConfig(options.messagesConfig); + } + + /** + * Read the api-extractor.json configuration and build up the tables of routing rules. + */ + private _applyMessagesConfig(messagesConfig: IExtractorMessagesConfig): void { + if (messagesConfig.compilerMessageReporting) { + for (const messageId of Object.getOwnPropertyNames(messagesConfig.compilerMessageReporting)) { + const reportingRule: IReportingRule = MessageRouter._getNormalizedRule( + messagesConfig.compilerMessageReporting[messageId]!, + ); + + if (messageId === 'default') { + this._compilerDefaultRule = reportingRule; + } else if (/^TS\d+$/.test(messageId)) { + this._reportingRuleByMessageId.set(messageId, reportingRule); + } else { + throw new Error( + `Error in API Extractor config: The messages.compilerMessageReporting table contains` + + ` an invalid entry "${messageId}". The identifier format is "TS" followed by an integer.`, + ); + } + } + } + + if (messagesConfig.extractorMessageReporting) { + for (const messageId of Object.getOwnPropertyNames(messagesConfig.extractorMessageReporting)) { + const reportingRule: IReportingRule = MessageRouter._getNormalizedRule( + messagesConfig.extractorMessageReporting[messageId]!, + ); + + if (messageId === 'default') { + this._extractorDefaultRule = reportingRule; + } else if (!messageId.startsWith('ae-')) { + throw new Error( + `Error in API Extractor config: The messages.extractorMessageReporting table contains` + + ` an invalid entry "${messageId}". The name should begin with the "ae-" prefix.`, + ); + } else if (allExtractorMessageIds.has(messageId)) { + this._reportingRuleByMessageId.set(messageId, reportingRule); + } else { + throw new Error( + `Error in API Extractor config: The messages.extractorMessageReporting table contains` + + ` an unrecognized identifier "${messageId}". Is it spelled correctly?`, + ); + } + } + } + + if (messagesConfig.tsdocMessageReporting) { + for (const messageId of Object.getOwnPropertyNames(messagesConfig.tsdocMessageReporting)) { + const reportingRule: IReportingRule = MessageRouter._getNormalizedRule( + messagesConfig.tsdocMessageReporting[messageId]!, + ); + + if (messageId === 'default') { + this._tsdocDefaultRule = reportingRule; + } else if (!messageId.startsWith('tsdoc-')) { + throw new Error( + `Error in API Extractor config: The messages.tsdocMessageReporting table contains` + + ` an invalid entry "${messageId}". The name should begin with the "tsdoc-" prefix.`, + ); + } else if (this._tsdocConfiguration.isKnownMessageId(messageId)) { + this._reportingRuleByMessageId.set(messageId, reportingRule); + } else { + throw new Error( + `Error in API Extractor config: The messages.tsdocMessageReporting table contains` + + ` an unrecognized identifier "${messageId}". Is it spelled correctly?`, + ); + } + } + } + } + + private static _getNormalizedRule(rule: IConfigMessageReportingRule): IReportingRule { + return { + logLevel: rule.logLevel || 'none', + addToApiReportFile: rule.addToApiReportFile ?? false, + }; + } + + public get messages(): readonly ExtractorMessage[] { + return this._messages; + } + + /** + * Add a diagnostic message reported by the TypeScript compiler + */ + public addCompilerDiagnostic(diagnostic: ts.Diagnostic): void { + switch (diagnostic.category) { + case ts.DiagnosticCategory.Suggestion: + case ts.DiagnosticCategory.Message: + return; // ignore noise + default: + break; + } + + const messageText: string = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + const options: IExtractorMessageOptions = { + category: ExtractorMessageCategory.Compiler, + messageId: `TS${diagnostic.code}`, + text: messageText, + }; + + if (diagnostic.file) { + // NOTE: Since compiler errors pertain to issues specific to the .d.ts files, + // we do not apply source mappings for them. + const sourceFile: ts.SourceFile = diagnostic.file; + const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({ + sourceFile, + pos: diagnostic.start ?? 0, + useDtsLocation: true, + }); + options.sourceFilePath = sourceLocation.sourceFilePath; + options.sourceFileLine = sourceLocation.sourceFileLine; + options.sourceFileColumn = sourceLocation.sourceFileColumn; + } + + this._messages.push(new ExtractorMessage(options)); + } + + /** + * Add a message from the API Extractor analysis + */ + public addAnalyzerIssue( + messageId: ExtractorMessageId, + messageText: string, + astDeclarationOrSymbol: AstDeclaration | AstSymbol, + properties?: IExtractorMessageProperties, + ): void { + let astDeclaration: AstDeclaration; + if (astDeclarationOrSymbol instanceof AstDeclaration) { + astDeclaration = astDeclarationOrSymbol; + } else { + astDeclaration = astDeclarationOrSymbol.astDeclarations[0]!; + } + + const extractorMessage: ExtractorMessage = this.addAnalyzerIssueForPosition( + messageId, + messageText, + astDeclaration.declaration.getSourceFile(), + astDeclaration.declaration.getStart(), + properties, + ); + + this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration); + } + + /** + * Add all messages produced from an invocation of the TSDoc parser, assuming they refer to + * code in the specified source file. + */ + public addTsdocMessages( + parserContext: tsdoc.ParserContext, + sourceFile: ts.SourceFile, + astDeclaration?: AstDeclaration, + ): void { + for (const message of parserContext.log.messages) { + const options: IExtractorMessageOptions = { + category: ExtractorMessageCategory.TSDoc, + messageId: message.messageId, + text: message.unformattedText, + }; + + const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({ + sourceFile, + pos: message.textRange.pos, + }); + options.sourceFilePath = sourceLocation.sourceFilePath; + options.sourceFileLine = sourceLocation.sourceFileLine; + options.sourceFileColumn = sourceLocation.sourceFileColumn; + + const extractorMessage: ExtractorMessage = new ExtractorMessage(options); + + if (astDeclaration) { + this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration); + } + + this._messages.push(extractorMessage); + } + } + + /** + * Recursively collects the primitive members (numbers, strings, arrays, etc) into an object that + * is JSON serializable. This is used by the "--diagnostics" feature to dump the state of configuration objects. + * + * @returns a JSON serializable object (possibly including `null` values) + * or `undefined` if the input cannot be represented as JSON + */ + + public static buildJsonDumpObject(input: any, options?: IBuildJsonDumpObjectOptions): any | undefined { + const ioptions = options ?? {}; + + const keyNamesToOmit: Set = new Set(ioptions.keyNamesToOmit); + + return MessageRouter._buildJsonDumpObject(input, keyNamesToOmit); + } + + private static _buildJsonDumpObject(input: any, keyNamesToOmit: Set): any | undefined { + if (input === null || input === undefined) { + return null; // JSON uses null instead of undefined + } + + switch (typeof input) { + case 'boolean': + case 'number': + case 'string': + return input; + case 'object': + if (Array.isArray(input)) { + const outputArray: any[] = []; + for (const element of input) { + const serializedElement: any = MessageRouter._buildJsonDumpObject(element, keyNamesToOmit); + if (serializedElement !== undefined) { + outputArray.push(serializedElement); + } + } + + return outputArray; + } + + // eslint-disable-next-line no-case-declarations + const outputObject: object = {}; + for (const key of Object.getOwnPropertyNames(input)) { + if (keyNamesToOmit.has(key)) { + continue; + } + + const value: any = input[key]; + + const serializedValue: any = MessageRouter._buildJsonDumpObject(value, keyNamesToOmit); + + if (serializedValue !== undefined) { + (outputObject as any)[key] = serializedValue; + } + } + + return outputObject; + default: + return undefined; + } + } + + /** + * Record this message in _associatedMessagesForAstDeclaration + */ + private _associateMessageWithAstDeclaration( + extractorMessage: ExtractorMessage, + astDeclaration: AstDeclaration, + ): void { + let associatedMessages: ExtractorMessage[] | undefined = + this._associatedMessagesForAstDeclaration.get(astDeclaration); + + if (!associatedMessages) { + associatedMessages = []; + this._associatedMessagesForAstDeclaration.set(astDeclaration, associatedMessages); + } + + associatedMessages.push(extractorMessage); + } + + /** + * Add a message for a location in an arbitrary source file. + */ + public addAnalyzerIssueForPosition( + messageId: ExtractorMessageId, + messageText: string, + sourceFile: ts.SourceFile, + pos: number, + properties?: IExtractorMessageProperties, + ): ExtractorMessage { + const options: IExtractorMessageOptions = { + category: ExtractorMessageCategory.Extractor, + messageId, + text: messageText, + properties, + }; + + const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({ + sourceFile, + pos, + }); + options.sourceFilePath = sourceLocation.sourceFilePath; + options.sourceFileLine = sourceLocation.sourceFileLine; + options.sourceFileColumn = sourceLocation.sourceFileColumn; + + const extractorMessage: ExtractorMessage = new ExtractorMessage(options); + + this._messages.push(extractorMessage); + return extractorMessage; + } + + /** + * This is used when writing the API report file. It looks up any messages that were configured to get emitted + * in the API report file and returns them. It also records that they were emitted, which suppresses them from + * being shown on the console. + */ + public fetchAssociatedMessagesForReviewFile(astDeclaration: AstDeclaration): ExtractorMessage[] { + const messagesForApiReportFile: ExtractorMessage[] = []; + + const associatedMessages: ExtractorMessage[] = this._associatedMessagesForAstDeclaration.get(astDeclaration) ?? []; + for (const associatedMessage of associatedMessages) { + // Make sure we didn't already report this message for some reason + if (!associatedMessage.handled) { + // Is this message type configured to go in the API report file? + const reportingRule: IReportingRule = this._getRuleForMessage(associatedMessage); + if (reportingRule.addToApiReportFile) { + // Include it in the result, and record that it went to the API report file + messagesForApiReportFile.push(associatedMessage); + associatedMessage.handled = true; + } + } + } + + this._sortMessagesForOutput(messagesForApiReportFile); + return messagesForApiReportFile; + } + + /** + * This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not + * retreieved using `fetchAssociatedMessagesForReviewFile()`. + */ + public fetchUnassociatedMessagesForReviewFile(): ExtractorMessage[] { + const messagesForApiReportFile: ExtractorMessage[] = []; + + for (const unassociatedMessage of this.messages) { + // Make sure we didn't already report this message for some reason + if (!unassociatedMessage.handled) { + // Is this message type configured to go in the API report file? + const reportingRule: IReportingRule = this._getRuleForMessage(unassociatedMessage); + if (reportingRule.addToApiReportFile) { + // Include it in the result, and record that it went to the API report file + messagesForApiReportFile.push(unassociatedMessage); + unassociatedMessage.handled = true; + } + } + } + + this._sortMessagesForOutput(messagesForApiReportFile); + return messagesForApiReportFile; + } + + /** + * This returns the list of remaining messages that were not already processed by + * `fetchAssociatedMessagesForReviewFile()` or `fetchUnassociatedMessagesForReviewFile()`. + * These messages will be shown on the console. + */ + public handleRemainingNonConsoleMessages(): void { + const messagesForLogger: ExtractorMessage[] = []; + + for (const message of this.messages) { + // Make sure we didn't already report this message + if (!message.handled) { + messagesForLogger.push(message); + } + } + + this._sortMessagesForOutput(messagesForLogger); + + for (const message of messagesForLogger) { + this._handleMessage(message); + } + } + + public logError(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void { + this._handleMessage( + new ExtractorMessage({ + category: ExtractorMessageCategory.Console, + messageId, + text: message, + properties, + logLevel: ExtractorLogLevel.Error, + }), + ); + } + + public logWarning(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void { + this._handleMessage( + new ExtractorMessage({ + category: ExtractorMessageCategory.Console, + messageId, + text: message, + properties, + logLevel: ExtractorLogLevel.Warning, + }), + ); + } + + public logInfo(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void { + this._handleMessage( + new ExtractorMessage({ + category: ExtractorMessageCategory.Console, + messageId, + text: message, + properties, + logLevel: ExtractorLogLevel.Info, + }), + ); + } + + public logVerbose(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void { + this._handleMessage( + new ExtractorMessage({ + category: ExtractorMessageCategory.Console, + messageId, + text: message, + properties, + logLevel: ExtractorLogLevel.Verbose, + }), + ); + } + + public logDiagnosticHeader(title: string): void { + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE); + this.logDiagnostic(`DIAGNOSTIC: ` + title); + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE); + } + + public logDiagnosticFooter(): void { + this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE + '\n'); + } + + public logDiagnostic(message: string): void { + if (this.showDiagnostics) { + this.logVerbose(ConsoleMessageId.Diagnostics, message); + } + } + + /** + * Give the calling application a chance to handle the `ExtractorMessage`, and if not, display it on the console. + */ + private _handleMessage(message: ExtractorMessage): void { + // Don't tally messages that were already "handled" by writing them into the API report + if (message.handled) { + return; + } + + // Assign the ExtractorMessage.logLevel; the message callback may adjust it below + if (message.category === ExtractorMessageCategory.Console) { + // Console messages have their category log level assigned via logInfo(), logVerbose(), etc. + } else { + const reportingRule: IReportingRule = this._getRuleForMessage(message); + message.logLevel = reportingRule.logLevel; + } + + // If there is a callback, allow it to modify and/or handle the message + if (this._messageCallback) { + this._messageCallback(message); + } + + // Update the statistics + switch (message.logLevel) { + case ExtractorLogLevel.Error: + ++this.errorCount; + break; + case ExtractorLogLevel.Warning: + ++this.warningCount; + break; + default: + break; + } + + if (message.handled) { + return; + } + + // The messageCallback did not handle the message, so perform default handling + message.handled = true; + + if (message.logLevel === ExtractorLogLevel.None) { + return; + } + + let messageText: string; + if (message.category === ExtractorMessageCategory.Console) { + messageText = message.text; + } else { + messageText = message.formatMessageWithLocation(this._workingPackageFolder); + } + + switch (message.logLevel) { + case ExtractorLogLevel.Error: + console.error(colors.red('Error: ' + messageText)); + break; + case ExtractorLogLevel.Warning: + console.warn(colors.yellow('Warning: ' + messageText)); + break; + case ExtractorLogLevel.Info: + console.log(messageText); + break; + case ExtractorLogLevel.Verbose: + if (this.showVerboseMessages) { + console.log(colors.cyan(messageText)); + } + + break; + default: + throw new Error(`Invalid logLevel value: ${JSON.stringify(message.logLevel)}`); + } + } + + /** + * For a given message, determine the IReportingRule based on the rule tables. + */ + private _getRuleForMessage(message: ExtractorMessage): IReportingRule { + const reportingRule: IReportingRule | undefined = this._reportingRuleByMessageId.get(message.messageId); + if (reportingRule) { + return reportingRule; + } + + switch (message.category) { + case ExtractorMessageCategory.Compiler: + return this._compilerDefaultRule; + case ExtractorMessageCategory.Extractor: + return this._extractorDefaultRule; + case ExtractorMessageCategory.TSDoc: + return this._tsdocDefaultRule; + case ExtractorMessageCategory.Console: + throw new InternalError('ExtractorMessageCategory.Console is not supported with IReportingRule'); + } + } + + /** + * Sorts an array of messages according to a reasonable ordering + */ + private _sortMessagesForOutput(messages: ExtractorMessage[]): void { + messages.sort((a, b) => { + let diff: number; + // First sort by file name + diff = Sort.compareByValue(a.sourceFilePath, b.sourceFilePath); + if (diff !== 0) { + return diff; + } + + // Then sort by line number + diff = Sort.compareByValue(a.sourceFileLine, b.sourceFileLine); + if (diff !== 0) { + return diff; + } + + // Then sort by messageId + return Sort.compareByValue(a.messageId, b.messageId); + }); + } +} diff --git a/packages/api-extractor/src/collector/SourceMapper.ts b/packages/api-extractor/src/collector/SourceMapper.ts new file mode 100644 index 000000000..130a6cae0 --- /dev/null +++ b/packages/api-extractor/src/collector/SourceMapper.ts @@ -0,0 +1,258 @@ +// 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, InternalError, JsonFile, NewlineKind } from '@rushstack/node-core-library'; +import { SourceMapConsumer, type RawSourceMap, type MappingItem, type Position } from 'source-map'; +import type ts from 'typescript'; + +interface ISourceMap { + // SourceMapConsumer.originalPositionFor() is useless because the mapping contains numerous gaps, + // and the API provides no way to find the nearest match. So instead we extract all the mapping items + // and search them using SourceMapper._findNearestMappingItem(). + mappingItems: MappingItem[]; + + sourceMapConsumer: SourceMapConsumer; +} + +interface IOriginalFileInfo { + // Whether the .ts file exists + fileExists: boolean; + + // This is used to check whether the guessed position is out of bounds. + // Since column/line numbers are 1-based, the 0th item in this array is unused. + maxColumnForLine: number[]; +} + +export interface ISourceLocation { + /** + * The column number in the source file. The first column number is 1. + */ + sourceFileColumn: number; + + /** + * The line number in the source file. The first line number is 1. + */ + sourceFileLine: number; + + /** + * The absolute path to the source file. + */ + sourceFilePath: string; +} + +export interface IGetSourceLocationOptions { + /** + * The position within the source file to get the source location from. + */ + pos: number; + + /** + * The source file to get the source location from. + */ + sourceFile: ts.SourceFile; + + /** + * If `false` or not provided, then we attempt to follow source maps in order to resolve the + * location to the original `.ts` file. If resolution isn't possible for some reason, we fall + * back to the `.d.ts` location. + * + * If `true`, then we don't bother following source maps, and the location refers to the `.d.ts` + * location. + */ + useDtsLocation?: boolean; +} + +export class SourceMapper { + // Map from .d.ts file path --> ISourceMap if a source map was found, or null if not found + private _sourceMapByFilePath: Map = new Map(); + + // Cache the FileSystem.exists() result for mapped .ts files + private _originalFileInfoByPath: Map = new Map(); + + /** + * Given a `.d.ts` source file and a specific position within the file, return the corresponding + * `ISourceLocation`. + */ + public getSourceLocation(options: IGetSourceLocationOptions): ISourceLocation { + const lineAndCharacter: ts.LineAndCharacter = options.sourceFile.getLineAndCharacterOfPosition(options.pos); + const sourceLocation: ISourceLocation = { + sourceFilePath: options.sourceFile.fileName, + sourceFileLine: lineAndCharacter.line + 1, + sourceFileColumn: lineAndCharacter.character + 1, + }; + + if (options.useDtsLocation) { + return sourceLocation; + } + + const mappedSourceLocation: ISourceLocation | undefined = this._getMappedSourceLocation(sourceLocation); + return mappedSourceLocation ?? sourceLocation; + } + + private _getMappedSourceLocation(sourceLocation: ISourceLocation): ISourceLocation | undefined { + const { sourceFilePath, sourceFileLine, sourceFileColumn } = sourceLocation; + + if (!FileSystem.exists(sourceFilePath)) { + // Sanity check + throw new InternalError('The referenced path was not found: ' + sourceFilePath); + } + + const sourceMap: ISourceMap | null = this._getSourceMap(sourceFilePath); + if (!sourceMap) return; + + const nearestMappingItem: MappingItem | undefined = SourceMapper._findNearestMappingItem(sourceMap.mappingItems, { + line: sourceFileLine, + column: sourceFileColumn, + }); + + if (!nearestMappingItem) return; + + const mappedFilePath: string = path.resolve(path.dirname(sourceFilePath), nearestMappingItem.source); + + // Does the mapped filename exist? Use a cache to remember the answer. + let originalFileInfo: IOriginalFileInfo | undefined = this._originalFileInfoByPath.get(mappedFilePath); + if (originalFileInfo === undefined) { + originalFileInfo = { + fileExists: FileSystem.exists(mappedFilePath), + maxColumnForLine: [], + }; + + if (originalFileInfo.fileExists) { + // Read the file and measure the length of each line + originalFileInfo.maxColumnForLine = FileSystem.readFile(mappedFilePath, { + convertLineEndings: NewlineKind.Lf, + }) + .split('\n') + .map((x) => x.length + 1); // +1 since columns are 1-based + originalFileInfo.maxColumnForLine.unshift(0); // Extra item since lines are 1-based + } + + this._originalFileInfoByPath.set(mappedFilePath, originalFileInfo); + } + + // Don't translate coordinates to a file that doesn't exist + if (!originalFileInfo.fileExists) return; + + // The nearestMappingItem anchor may be above/left of the real position, due to gaps in the mapping. Calculate + // the delta and apply it to the original position. + const guessedPosition: Position = { + line: nearestMappingItem.originalLine + sourceFileLine - nearestMappingItem.generatedLine, + column: nearestMappingItem.originalColumn + sourceFileColumn - nearestMappingItem.generatedColumn, + }; + + // Verify that the result is not out of bounds, in cause our heuristic failed + if ( + guessedPosition.line >= 1 && + guessedPosition.line < originalFileInfo.maxColumnForLine.length && + guessedPosition.column >= 1 && + guessedPosition.column <= originalFileInfo.maxColumnForLine[guessedPosition.line]! + ) { + return { + sourceFilePath: mappedFilePath, + sourceFileLine: guessedPosition.line, + sourceFileColumn: guessedPosition.column, + }; + } else { + // The guessed position was out of bounds, so use the nearestMappingItem position instead. + return { + sourceFilePath: mappedFilePath, + sourceFileLine: nearestMappingItem.originalLine, + sourceFileColumn: nearestMappingItem.originalColumn, + }; + } + } + + private _getSourceMap(sourceFilePath: string): ISourceMap | null { + let sourceMap: ISourceMap | null | undefined = this._sourceMapByFilePath.get(sourceFilePath); + + if (sourceMap === undefined) { + // Normalize the path and redo the lookup + const normalizedPath: string = FileSystem.getRealPath(sourceFilePath); + + sourceMap = this._sourceMapByFilePath.get(normalizedPath); + if (sourceMap === undefined) { + // Given "folder/file.d.ts", check for a corresponding "folder/file.d.ts.map" + const sourceMapPath: string = normalizedPath + '.map'; + if (FileSystem.exists(sourceMapPath)) { + // Load up the source map + const rawSourceMap: RawSourceMap = JsonFile.load(sourceMapPath) as RawSourceMap; + + const sourceMapConsumer: SourceMapConsumer = new SourceMapConsumer(rawSourceMap); + const mappingItems: MappingItem[] = []; + + // Extract the list of mapping items + sourceMapConsumer.eachMapping( + (mappingItem: MappingItem) => { + mappingItems.push({ + ...mappingItem, + // The "source-map" package inexplicably uses 1-based line numbers but 0-based column numbers. + // Fix that up proactively so we don't have to deal with it later. + generatedColumn: mappingItem.generatedColumn + 1, + originalColumn: mappingItem.originalColumn + 1, + }); + }, + this, + SourceMapConsumer.GENERATED_ORDER, + ); + + sourceMap = { sourceMapConsumer, mappingItems }; + } else { + // No source map for this filename + sourceMap = null; + } + + this._sourceMapByFilePath.set(normalizedPath, sourceMap); + if (sourceFilePath !== normalizedPath) { + // Add both keys to the map + this._sourceMapByFilePath.set(sourceFilePath, sourceMap); + } + } else { + // Copy the result from the normalized to the non-normalized key + this._sourceMapByFilePath.set(sourceFilePath, sourceMap); + } + } + + return sourceMap; + } + + // The `mappingItems` array is sorted by generatedLine/generatedColumn (GENERATED_ORDER). + // The _findNearestMappingItem() lookup is a simple binary search that returns the previous item + // if there is no exact match. + private static _findNearestMappingItem(mappingItems: MappingItem[], position: Position): MappingItem | undefined { + if (mappingItems.length === 0) { + return undefined; + } + + let startIndex = 0; + let endIndex: number = mappingItems.length - 1; + + while (startIndex <= endIndex) { + const middleIndex: number = startIndex + Math.floor((endIndex - startIndex) / 2); + + const diff: number = SourceMapper._compareMappingItem(mappingItems[middleIndex]!, position); + + if (diff < 0) { + startIndex = middleIndex + 1; + } else if (diff > 0) { + endIndex = middleIndex - 1; + } else { + // Exact match + return mappingItems[middleIndex]; + } + } + + // If we didn't find an exact match, then endIndex < startIndex. + // Take endIndex because it's the smaller value. + return mappingItems[endIndex]; + } + + private static _compareMappingItem(mappingItem: MappingItem, position: Position): number { + const diff: number = mappingItem.generatedLine - position.line; + if (diff !== 0) { + return diff; + } + + return mappingItem.generatedColumn - position.column; + } +} diff --git a/packages/api-extractor/src/collector/SymbolMetadata.ts b/packages/api-extractor/src/collector/SymbolMetadata.ts new file mode 100644 index 000000000..e688784f5 --- /dev/null +++ b/packages/api-extractor/src/collector/SymbolMetadata.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { ReleaseTag } from '@discordjs/api-extractor-model'; + +/** + * Constructor parameters for `SymbolMetadata`. + */ +export interface ISymbolMetadataOptions { + maxEffectiveReleaseTag: ReleaseTag; +} + +/** + * Stores the Collector's additional analysis for an `AstSymbol`. This object is assigned to `AstSymbol.metadata` + * but consumers must always obtain it by calling `Collector.fetchSymbolMetadata()`. + */ +export class SymbolMetadata { + // For all declarations associated with this symbol, this is the + // `ApiItemMetadata.effectiveReleaseTag` value that is most public. + public readonly maxEffectiveReleaseTag: ReleaseTag; + + public constructor(options: ISymbolMetadataOptions) { + this.maxEffectiveReleaseTag = options.maxEffectiveReleaseTag; + } +} diff --git a/packages/api-extractor/src/collector/VisitorState.ts b/packages/api-extractor/src/collector/VisitorState.ts new file mode 100644 index 000000000..12c1354da --- /dev/null +++ b/packages/api-extractor/src/collector/VisitorState.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * Keeps track of a directed graph traversal that needs to detect cycles. + */ +export enum VisitorState { + /** + * We have not visited the node yet. + */ + Unvisited = 0, + + /** + * We have visited the node, but have not finished traversing its references yet. + * If we reach a node that is already in the `Visiting` state, this means we have + * encountered a cyclic reference. + */ + Visiting = 1, + + /** + * We are finished vising the node and all its references. + */ + Visited = 2, +} diff --git a/packages/api-extractor/src/collector/WorkingPackage.ts b/packages/api-extractor/src/collector/WorkingPackage.ts new file mode 100644 index 000000000..2bbbb0de2 --- /dev/null +++ b/packages/api-extractor/src/collector/WorkingPackage.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as tsdoc from '@microsoft/tsdoc'; +import type { INodePackageJson } from '@rushstack/node-core-library'; +import type * as ts from 'typescript'; + +/** + * Constructor options for WorkingPackage + */ +export interface IWorkingPackageOptions { + entryPointSourceFile: ts.SourceFile; + packageFolder: string; + packageJson: INodePackageJson; +} + +/** + * Information about the working package for a particular invocation of API Extractor. + * + * @remarks + * API Extractor tries to model the world as a collection of NPM packages, such that each + * .d.ts file belongs to at most one package. When API Extractor is invoked on a project, + * we refer to that project as being the "working package". There is exactly one + * "working package" for the duration of this analysis. Any files that do not belong to + * the working package are referred to as "external": external declarations belonging to + * external packages. + * + * If API Extractor is invoked on a standalone .d.ts file, the "working package" may not + * have an actual package.json file on disk, but we still refer to it in concept. + */ +export class WorkingPackage { + /** + * Returns the folder for the package.json file of the working package. + * + * @remarks + * If the entry point is `C:\Folder\project\src\index.ts` and the nearest package.json + * is `C:\Folder\project\package.json`, then the packageFolder is `C:\Folder\project` + */ + public readonly packageFolder: string; + + /** + * The parsed package.json file for the working package. + */ + public readonly packageJson: INodePackageJson; + + /** + * The entry point being processed during this invocation of API Extractor. + * + * @remarks + * The working package may have multiple entry points; however, today API Extractor + * only processes a single entry point during an invocation. This will be improved + * in the future. + */ + public readonly entryPointSourceFile: ts.SourceFile; + + /** + * The `@packageDocumentation` comment, if any, for the working package. + */ + public tsdocComment: tsdoc.DocComment | undefined; + + /** + * Additional parser information for `WorkingPackage.tsdocComment`. + */ + public tsdocParserContext: tsdoc.ParserContext | undefined; + + public constructor(options: IWorkingPackageOptions) { + this.packageFolder = options.packageFolder; + this.packageJson = options.packageJson; + this.entryPointSourceFile = options.entryPointSourceFile; + } + + /** + * Returns the full name of the working package. + */ + public get name(): string { + return this.packageJson.name; + } +} diff --git a/packages/api-extractor/src/enhancers/DocCommentEnhancer.ts b/packages/api-extractor/src/enhancers/DocCommentEnhancer.ts new file mode 100644 index 000000000..5b97e69ce --- /dev/null +++ b/packages/api-extractor/src/enhancers/DocCommentEnhancer.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ReleaseTag } from '@discordjs/api-extractor-model'; +import * as tsdoc from '@microsoft/tsdoc'; +import * as ts from 'typescript'; +import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; +import { ResolverFailure } from '../analyzer/AstReferenceResolver.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +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'; + +export class DocCommentEnhancer { + private readonly _collector: Collector; + + public constructor(collector: Collector) { + this._collector = collector; + } + + public static analyze(collector: Collector): void { + const docCommentEnhancer: DocCommentEnhancer = new DocCommentEnhancer(collector); + docCommentEnhancer.analyze(); + } + + 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); + }); + } + } + } + + private _analyzeApiItem(astDeclaration: AstDeclaration): void { + const metadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + if (metadata.docCommentEnhancerVisitorState === VisitorState.Visited) { + return; + } + + if (metadata.docCommentEnhancerVisitorState === VisitorState.Visiting) { + this._collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.CyclicInheritDoc, + `The @inheritDoc tag for "${astDeclaration.astSymbol.localName}" refers to its own declaration`, + astDeclaration, + ); + return; + } + + metadata.docCommentEnhancerVisitorState = VisitorState.Visiting; + + if (metadata.tsdocComment?.inheritDocTag) { + this._applyInheritDoc(astDeclaration, metadata.tsdocComment, metadata.tsdocComment.inheritDocTag); + } + + this._analyzeNeedsDocumentation(astDeclaration, metadata); + + this._checkForBrokenLinks(astDeclaration, metadata); + + metadata.docCommentEnhancerVisitorState = VisitorState.Visited; + } + + private _analyzeNeedsDocumentation(astDeclaration: AstDeclaration, metadata: ApiItemMetadata): void { + if (astDeclaration.declaration.kind === ts.SyntaxKind.Constructor) { + // Constructors always do pretty much the same thing, so it's annoying to require people to write + // descriptions for them. Instead, if the constructor lacks a TSDoc summary, then API Extractor + // will auto-generate one. + metadata.undocumented = false; + + // The class that contains this constructor + const classDeclaration: AstDeclaration = astDeclaration.parent!; + + const configuration: tsdoc.TSDocConfiguration = this._collector.extractorConfig.tsdocConfiguration; + + if (!metadata.tsdocComment) { + metadata.tsdocComment = new tsdoc.DocComment({ configuration }); + } + + if (!tsdoc.PlainTextEmitter.hasAnyTextContent(metadata.tsdocComment.summarySection)) { + metadata.tsdocComment.summarySection.appendNodesInParagraph([ + new tsdoc.DocPlainText({ configuration, text: 'Constructs a new instance of the ' }), + new tsdoc.DocCodeSpan({ + configuration, + code: classDeclaration.astSymbol.localName, + }), + new tsdoc.DocPlainText({ configuration, text: ' class' }), + ]); + } + + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + if (apiItemMetadata.effectiveReleaseTag === ReleaseTag.Internal) { + // If the constructor is marked as internal, then add a boilerplate notice for the containing class + const classMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(classDeclaration); + + if (!classMetadata.tsdocComment) { + classMetadata.tsdocComment = new tsdoc.DocComment({ configuration }); + } + + if (classMetadata.tsdocComment.remarksBlock === undefined) { + classMetadata.tsdocComment.remarksBlock = new tsdoc.DocBlock({ + configuration, + blockTag: new tsdoc.DocBlockTag({ + configuration, + tagName: tsdoc.StandardTags.remarks.tagName, + }), + }); + } + + classMetadata.tsdocComment.remarksBlock.content.appendNode( + new tsdoc.DocParagraph({ configuration }, [ + new tsdoc.DocPlainText({ + configuration, + text: + `The constructor for this class is marked as internal. Third-party code should not` + + ` call the constructor directly or create subclasses that extend the `, + }), + new tsdoc.DocCodeSpan({ + configuration, + code: classDeclaration.astSymbol.localName, + }), + new tsdoc.DocPlainText({ configuration, text: ' class.' }), + ]), + ); + } + + return; + } + + if (metadata.tsdocComment) { + // Require the summary to contain at least 10 non-spacing characters + metadata.undocumented = !tsdoc.PlainTextEmitter.hasAnyTextContent(metadata.tsdocComment.summarySection, 10); + } else { + metadata.undocumented = true; + } + } + + private _checkForBrokenLinks(astDeclaration: AstDeclaration, metadata: ApiItemMetadata): void { + if (!metadata.tsdocComment) { + return; + } + + this._checkForBrokenLinksRecursive(astDeclaration, metadata.tsdocComment); + } + + private _checkForBrokenLinksRecursive(astDeclaration: AstDeclaration, node: tsdoc.DocNode): 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 + // AstReferenceResolver doesn't support it yet (but ModelReferenceResolver does of course). + // Tracked by: https://github.com/microsoft/rushstack/issues/1195 + (node.codeDestination.packageName === undefined || + node.codeDestination.packageName === this._collector.workingPackage.name) + ) { + const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve( + node.codeDestination, + ); + + if (referencedAstDeclaration instanceof ResolverFailure) { + this._collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.UnresolvedLink, + 'The @link reference could not be resolved: ' + referencedAstDeclaration.reason, + astDeclaration, + ); + } + } + + for (const childNode of node.getChildNodes()) { + this._checkForBrokenLinksRecursive(astDeclaration, childNode); + } + } + + /** + * Follow an `{@inheritDoc ___}` reference and copy the content that we find in the referenced comment. + */ + private _applyInheritDoc( + astDeclaration: AstDeclaration, + docComment: tsdoc.DocComment, + inheritDocTag: tsdoc.DocInheritDocTag, + ): void { + if (!inheritDocTag.declarationReference) { + this._collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.UnresolvedInheritDocBase, + 'The @inheritDoc tag needs a TSDoc declaration reference; signature matching is not supported yet', + astDeclaration, + ); + return; + } + + // Is it referring to the working package? + if ( + !( + inheritDocTag.declarationReference.packageName === undefined || + inheritDocTag.declarationReference.packageName === this._collector.workingPackage.name + ) + ) { + // It's referencing an external package, so skip this inheritDoc tag, since AstReferenceResolver doesn't + // support it yet. As a workaround, this tag will get handled later by api-documenter. + // Tracked by: https://github.com/microsoft/rushstack/issues/1195 + return; + } + + const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve( + inheritDocTag.declarationReference, + ); + + if (referencedAstDeclaration instanceof ResolverFailure) { + this._collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.UnresolvedInheritDocReference, + 'The @inheritDoc reference could not be resolved: ' + referencedAstDeclaration.reason, + astDeclaration, + ); + return; + } + + this._analyzeApiItem(referencedAstDeclaration); + + const referencedMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(referencedAstDeclaration); + + if (referencedMetadata.tsdocComment) { + this._copyInheritedDocs(docComment, referencedMetadata.tsdocComment); + } + } + + /** + * Copy the content from `sourceDocComment` to `targetDocComment`. + */ + private _copyInheritedDocs(targetDocComment: tsdoc.DocComment, sourceDocComment: tsdoc.DocComment): void { + targetDocComment.summarySection = sourceDocComment.summarySection; + targetDocComment.remarksBlock = sourceDocComment.remarksBlock; + + targetDocComment.params.clear(); + for (const param of sourceDocComment.params) { + targetDocComment.params.add(param); + } + + for (const typeParam of sourceDocComment.typeParams) { + targetDocComment.typeParams.add(typeParam); + } + + targetDocComment.returnsBlock = sourceDocComment.returnsBlock; + + targetDocComment.inheritDocTag = undefined; + } +} diff --git a/packages/api-extractor/src/enhancers/ValidationEnhancer.ts b/packages/api-extractor/src/enhancers/ValidationEnhancer.ts new file mode 100644 index 000000000..1eaaafd36 --- /dev/null +++ b/packages/api-extractor/src/enhancers/ValidationEnhancer.ts @@ -0,0 +1,294 @@ +// 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 { ReleaseTag, releaseTagCompare, releaseTagGetTagName } from '@discordjs/api-extractor-model'; +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 { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +import { ExtractorMessageId } from '../api/ExtractorMessageId.js'; +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'; + +export class ValidationEnhancer { + public static analyze(collector: Collector): void { + const alreadyWarnedEntities: Set = new Set(); + + for (const entity of collector.entities) { + if ( + !( + entity.consumable || + collector.extractorConfig.apiReportIncludeForgottenExports || + collector.extractorConfig.docModelIncludeForgottenExports + ) + ) { + continue; + } + + if (entity.astEntity instanceof AstSymbol) { + // A regular exported AstSymbol + + const astSymbol: AstSymbol = entity.astEntity; + + astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities); + }); + + 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); + + for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) { + if (namespaceMemberAstEntity instanceof AstSymbol) { + const astSymbol: AstSymbol = namespaceMemberAstEntity; + + astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => { + ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities); + }); + + const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol); + + // (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members) + + ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata); + } + } + } + } + } + + private static _checkForInternalUnderscore( + collector: Collector, + collectorEntity: CollectorEntity, + astSymbol: AstSymbol, + symbolMetadata: SymbolMetadata, + ): void { + let needsUnderscore = false; + + if (symbolMetadata.maxEffectiveReleaseTag === ReleaseTag.Internal) { + if (astSymbol.parentAstSymbol) { + // If it's marked as @internal and the parent isn't obviously already @internal, then it needs an underscore. + // + // For example, we WOULD need an underscore for a merged declaration like this: + // + // /** @internal */ + // export namespace X { + // export interface _Y { } + // } + // + // /** @public */ + // export class X { + // /** @internal */ + // public static _Y(): void { } // <==== different from parent + // } + const parentSymbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol); + if (parentSymbolMetadata.maxEffectiveReleaseTag > ReleaseTag.Internal) { + needsUnderscore = true; + } + } else { + // If it's marked as @internal and has no parent, then it needs an underscore. + // We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this: + // + // /** @public */ + // export enum X { } + // + // /** @internal */ + // export namespace X { } + // + // (The above normally reports an error "ae-different-release-tags", but that may be suppressed.) + needsUnderscore = true; + } + } + + if (needsUnderscore) { + for (const exportName of collectorEntity.exportNames) { + if (!exportName.startsWith('_')) { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.InternalMissingUnderscore, + `The name "${exportName}" should be prefixed with an underscore` + + ` because the declaration is marked as @internal`, + astSymbol, + { exportName }, + ); + } + } + } + } + + private static _checkForInconsistentReleaseTags( + collector: Collector, + astSymbol: AstSymbol, + symbolMetadata: SymbolMetadata, + ): void { + if (astSymbol.isExternal) { + // For now, don't report errors for external code. If the developer cares about it, they should run + // API Extractor separately on the external project + return; + } + + // Normally we will expect all release tags to be the same. Arbitrarily we choose the maxEffectiveReleaseTag + // as the thing they should all match. + const expectedEffectiveReleaseTag: ReleaseTag = symbolMetadata.maxEffectiveReleaseTag; + + // This is set to true if we find a declaration whose release tag is different from expectedEffectiveReleaseTag + let mixedReleaseTags = false; + + // This is set to false if we find a declaration that is not a function/method overload + let onlyFunctionOverloads = true; + + // This is set to true if we find a declaration that is @internal + let anyInternalReleaseTags = false; + + for (const astDeclaration of astSymbol.astDeclarations) { + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + const effectiveReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodDeclaration: + break; + default: + onlyFunctionOverloads = false; + } + + if (effectiveReleaseTag !== expectedEffectiveReleaseTag) { + mixedReleaseTags = true; + } + + if (effectiveReleaseTag === ReleaseTag.Internal) { + anyInternalReleaseTags = true; + } + } + + if (mixedReleaseTags) { + if (!onlyFunctionOverloads) { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.DifferentReleaseTags, + 'This symbol has another declaration with a different release tag', + astSymbol, + ); + } + + if (anyInternalReleaseTags) { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.InternalMixedReleaseTag, + `Mixed release tags are not allowed for "${astSymbol.localName}" because one of its declarations` + + ` is marked as @internal`, + astSymbol, + ); + } + } + } + + private static _checkReferences( + collector: Collector, + astDeclaration: AstDeclaration, + alreadyWarnedEntities: Set, + ): void { + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + const declarationReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + + for (const referencedEntity of astDeclaration.referencedAstEntities) { + let collectorEntity: CollectorEntity | undefined; + let referencedReleaseTag: ReleaseTag; + let localName: string; + + if (referencedEntity instanceof AstSymbol) { + // If this is e.g. a member of a namespace, then we need to be checking the top-level scope to see + // whether it's exported. + // + // Technically we should also check each of the nested scopes along the way. + const rootSymbol: AstSymbol = referencedEntity.rootAstSymbol; + + if (rootSymbol.isExternal) { + continue; + } + + collectorEntity = collector.tryGetCollectorEntity(rootSymbol); + localName = collectorEntity?.nameForEmit ?? rootSymbol.localName; + + const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity); + referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag; + } else if (referencedEntity instanceof AstNamespaceImport) { + collectorEntity = collector.tryGetCollectorEntity(referencedEntity); + + referencedReleaseTag = ReleaseTag.Public; + + localName = collectorEntity?.nameForEmit ?? referencedEntity.localName; + } else { + continue; + } + + if (collectorEntity && collectorEntity.consumable) { + if (releaseTagCompare(declarationReleaseTag, referencedReleaseTag) > 0) { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.IncompatibleReleaseTags, + `The symbol "${astDeclaration.astSymbol.localName}"` + + ` is marked as ${releaseTagGetTagName(declarationReleaseTag)},` + + ` but its signature references "${localName}"` + + ` which is marked as ${releaseTagGetTagName(referencedReleaseTag)}`, + astDeclaration, + ); + } + } else { + const entryPointFilename: string = path.basename(collector.workingPackage.entryPointSourceFile.fileName); + + if (!alreadyWarnedEntities.has(referencedEntity)) { + alreadyWarnedEntities.add(referencedEntity); + + if (referencedEntity instanceof AstSymbol && ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) { + // The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object, + // so as a special case, we do NOT report them as forgotten exports. + } else { + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.ForgottenExport, + `The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`, + astDeclaration, + ); + } + } + } + } + } + + // Detect an AstSymbol that refers to an ECMAScript symbol declaration such as: + // + // const mySymbol: unique symbol = Symbol('mySymbol'); + private static _isEcmaScriptSymbol(astSymbol: AstSymbol): boolean { + if (astSymbol.astDeclarations.length !== 1) { + return false; + } + + // We are matching a form like this: + // + // - VariableDeclaration: + // - Identifier: pre=[mySymbol] + // - ColonToken: pre=[:] sep=[ ] + // - TypeOperator: + // - UniqueKeyword: pre=[unique] sep=[ ] + // - SymbolKeyword: pre=[symbol] + const astDeclaration: AstDeclaration = astSymbol.astDeclarations[0]!; + if (ts.isVariableDeclaration(astDeclaration.declaration)) { + const variableTypeNode: ts.TypeNode | undefined = astDeclaration.declaration.type; + if (variableTypeNode) { + for (const token of variableTypeNode.getChildren()) { + if (token.kind === ts.SyntaxKind.SymbolKeyword) { + return true; + } + } + } + } + + return false; + } +} diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts new file mode 100644 index 000000000..b30d46ccd --- /dev/null +++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts @@ -0,0 +1,1134 @@ +// 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 { + ApiModel, + ApiClass, + ApiPackage, + ApiEntryPoint, + ApiMethod, + ApiNamespace, + ApiInterface, + ApiPropertySignature, + type ApiItemContainerMixin, + ReleaseTag, + ApiProperty, + ApiMethodSignature, + type IApiParameterOptions, + ApiEnum, + ApiEnumMember, + type IExcerptTokenRange, + type IExcerptToken, + ApiConstructor, + ApiConstructSignature, + ApiFunction, + ApiIndexSignature, + ApiVariable, + ApiTypeAlias, + ApiCallSignature, + type IApiTypeParameterOptions, + EnumMemberOrder, +} from '@discordjs/api-extractor-model'; +import type * as tsdoc from '@microsoft/tsdoc'; +import { Path } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; +import type { AstEntity } from '../analyzer/AstEntity.js'; +import type { 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 { ApiItemMetadata } from '../collector/ApiItemMetadata.js'; +import type { Collector } from '../collector/Collector.js'; +import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js'; +import type { ISourceLocation } from '../collector/SourceMapper.js'; +import { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator.js'; +import { ExcerptBuilder, type IExcerptBuilderNodeToCapture } from './ExcerptBuilder.js'; + +interface IProcessAstEntityContext { + isExported: boolean; + name: string; + parentApiItem: ApiItemContainerMixin; +} + +export class ApiModelGenerator { + private readonly _collector: Collector; + + private readonly _apiModel: ApiModel; + + private readonly _referenceGenerator: DeclarationReferenceGenerator; + + public constructor(collector: Collector) { + this._collector = collector; + this._apiModel = new ApiModel(); + this._referenceGenerator = new DeclarationReferenceGenerator(collector); + } + + public get apiModel(): ApiModel { + return this._apiModel; + } + + public buildApiPackage(): ApiPackage { + const packageDocComment: tsdoc.DocComment | undefined = this._collector.workingPackage.tsdocComment; + + const apiPackage: ApiPackage = new ApiPackage({ + name: this._collector.workingPackage.name, + docComment: packageDocComment, + tsdocConfiguration: this._collector.extractorConfig.tsdocConfiguration, + projectFolderUrl: this._collector.extractorConfig.projectFolderUrl, + }); + this._apiModel.addMember(apiPackage); + + const apiEntryPoint: ApiEntryPoint = new ApiEntryPoint({ name: '' }); + 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, + }); + } + } + + return apiPackage; + } + + private _processAstEntity(astEntity: AstEntity, context: IProcessAstEntityContext): void { + if (astEntity instanceof AstSymbol) { + // Skip ancillary declarations; we will process them with the main declaration + for (const astDeclaration of this._collector.getNonAncillaryDeclarations(astEntity)) { + this._processDeclaration(astDeclaration, context); + } + + return; + } + + if (astEntity instanceof AstNamespaceImport) { + // Note that a single API item can belong to two different AstNamespaceImport namespaces. For example: + // + // // file.ts defines "thing()" + // import * as example1 from "./file"; + // import * as example2 from "./file"; + // + // // ...so here we end up with example1.thing() and example2.thing() + // export { example1, example2 } + // + // The current logic does not try to associate "thing()" with a specific parent. Instead + // the API documentation will show duplicated entries for example1.thing() and example2.thing(). + // + // This could be improved in the future, but it requires a stable mechanism for choosing an associated parent. + // For thoughts about this: https://github.com/microsoft/rushstack/issues/1308 + this._processAstNamespaceImport(astEntity, context); + } + + // TO DO: Figure out how to represent reexported AstImport objects. Basically we need to introduce a new + // ApiItem subclass for "export alias", similar to a type alias, but representing declarations of the + // form "export { X } from 'external-package'". We can also use this to solve GitHub issue #950. + } + + private _processAstNamespaceImport(astNamespaceImport: AstNamespaceImport, context: IProcessAstEntityContext): void { + const astModule: AstModule = astNamespaceImport.astModule; + const { name, isExported, parentApiItem } = context; + const containerKey: string = ApiNamespace.getContainerKey(name); + const sourceLocation: ISourceLocation = this._getSourceLocation(astNamespaceImport.declaration); + + let apiNamespace: ApiNamespace | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiNamespace; + + if (apiNamespace === undefined) { + apiNamespace = new ApiNamespace({ + name, + docComment: undefined, + releaseTag: ReleaseTag.None, + excerptTokens: [], + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + parentApiItem.addMember(apiNamespace); + } + + // eslint-disable-next-line unicorn/no-array-for-each + astModule.astModuleExportInfo!.exportedLocalEntities.forEach((exportedEntity: AstEntity, exportedName: string) => { + this._processAstEntity(exportedEntity, { + name: exportedName, + isExported: true, + parentApiItem: apiNamespace!, + }); + }); + } + + private _processDeclaration(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { + return; // trim out private declarations + } + + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + if (releaseTag === ReleaseTag.Internal) { + return; // trim out items marked as "@internal" + } + + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.CallSignature: + this._processApiCallSignature(astDeclaration, context); + break; + + case ts.SyntaxKind.Constructor: + this._processApiConstructor(astDeclaration, context); + break; + + case ts.SyntaxKind.ConstructSignature: + this._processApiConstructSignature(astDeclaration, context); + break; + + case ts.SyntaxKind.ClassDeclaration: + this._processApiClass(astDeclaration, context); + break; + + case ts.SyntaxKind.EnumDeclaration: + this._processApiEnum(astDeclaration, context); + break; + + case ts.SyntaxKind.EnumMember: + this._processApiEnumMember(astDeclaration, context); + break; + + case ts.SyntaxKind.FunctionDeclaration: + this._processApiFunction(astDeclaration, context); + break; + + case ts.SyntaxKind.GetAccessor: + this._processApiProperty(astDeclaration, context); + break; + + case ts.SyntaxKind.SetAccessor: + this._processApiProperty(astDeclaration, context); + break; + + case ts.SyntaxKind.IndexSignature: + this._processApiIndexSignature(astDeclaration, context); + break; + + case ts.SyntaxKind.InterfaceDeclaration: + this._processApiInterface(astDeclaration, context); + break; + + case ts.SyntaxKind.MethodDeclaration: + this._processApiMethod(astDeclaration, context); + break; + + case ts.SyntaxKind.MethodSignature: + this._processApiMethodSignature(astDeclaration, context); + break; + + case ts.SyntaxKind.ModuleDeclaration: + this._processApiNamespace(astDeclaration, context); + break; + + case ts.SyntaxKind.PropertyDeclaration: + this._processApiProperty(astDeclaration, context); + break; + + case ts.SyntaxKind.PropertySignature: + this._processApiPropertySignature(astDeclaration, context); + break; + + case ts.SyntaxKind.TypeAliasDeclaration: + this._processApiTypeAlias(astDeclaration, context); + break; + + case ts.SyntaxKind.VariableDeclaration: + this._processApiVariable(astDeclaration, context); + break; + + default: + // ignore unknown types + } + } + + private _processChildDeclarations(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + for (const childDeclaration of astDeclaration.children) { + this._processDeclaration(childDeclaration, { + ...context, + name: childDeclaration.astSymbol.localName, + }); + } + } + + private _processApiCallSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { parentApiItem } = context; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiCallSignature.getContainerKey(overloadIndex); + + let apiCallSignature: ApiCallSignature | undefined = parentApiItem.tryGetMemberByKey( + containerKey, + ) as ApiCallSignature; + + if (apiCallSignature === undefined) { + const callSignature: ts.CallSignatureDeclaration = astDeclaration.declaration as ts.CallSignatureDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: callSignature.type, tokenRange: returnTypeTokenRange }); + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + callSignature.typeParameters, + ); + + const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, callSignature.parameters); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(callSignature); + + apiCallSignature = new ApiCallSignature({ + docComment, + releaseTag, + typeParameters, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiCallSignature); + } + } + + private _processApiConstructor(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { parentApiItem } = context; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiConstructor.getContainerKey(overloadIndex); + + let apiConstructor: ApiConstructor | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiConstructor; + + if (apiConstructor === undefined) { + const constructorDeclaration: ts.ConstructorDeclaration = astDeclaration.declaration as ts.ConstructorDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const parameters: IApiParameterOptions[] = this._captureParameters( + nodesToCapture, + constructorDeclaration.parameters, + ); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; + const sourceLocation: ISourceLocation = this._getSourceLocation(constructorDeclaration); + + apiConstructor = new ApiConstructor({ + docComment, + releaseTag, + isProtected, + parameters, + overloadIndex, + excerptTokens, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiConstructor); + } + } + + private _processApiClass(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + const containerKey: string = ApiClass.getContainerKey(name); + + let apiClass: ApiClass | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiClass; + + if (apiClass === undefined) { + const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + classDeclaration.typeParameters, + ); + + let extendsTokenRange: IExcerptTokenRange | undefined; + const implementsTokenRanges: IExcerptTokenRange[] = []; + + for (const heritageClause of classDeclaration.heritageClauses ?? []) { + if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { + extendsTokenRange = ExcerptBuilder.createEmptyTokenRange(); + if (heritageClause.types.length > 0) { + nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange }); + } + } else if (heritageClause.token === ts.SyntaxKind.ImplementsKeyword) { + for (const heritageType of heritageClause.types) { + const implementsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + implementsTokenRanges.push(implementsTokenRange); + nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange }); + } + } + } + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isAbstract: boolean = (ts.getCombinedModifierFlags(classDeclaration) & ts.ModifierFlags.Abstract) !== 0; + const sourceLocation: ISourceLocation = this._getSourceLocation(classDeclaration); + + apiClass = new ApiClass({ + name, + isAbstract, + docComment, + releaseTag, + excerptTokens, + typeParameters, + extendsTokenRange, + implementsTokenRanges, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiClass); + } + + this._processChildDeclarations(astDeclaration, { + ...context, + parentApiItem: apiClass, + }); + } + + private _processApiConstructSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { parentApiItem } = context; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiConstructSignature.getContainerKey(overloadIndex); + + let apiConstructSignature: ApiConstructSignature | undefined = parentApiItem.tryGetMemberByKey( + containerKey, + ) as ApiConstructSignature; + + if (apiConstructSignature === undefined) { + const constructSignature: ts.ConstructSignatureDeclaration = + astDeclaration.declaration as ts.ConstructSignatureDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: constructSignature.type, tokenRange: returnTypeTokenRange }); + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + constructSignature.typeParameters, + ); + + const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, constructSignature.parameters); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(constructSignature); + + apiConstructSignature = new ApiConstructSignature({ + docComment, + releaseTag, + typeParameters, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiConstructSignature); + } + } + + private _processApiEnum(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + const containerKey: string = ApiEnum.getContainerKey(name); + + let apiEnum: ApiEnum | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiEnum; + + if (apiEnum === undefined) { + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, []); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const preserveMemberOrder: boolean = this._collector.extractorConfig.enumMemberOrder === EnumMemberOrder.Preserve; + const sourceLocation: ISourceLocation = this._getSourceLocation(astDeclaration.declaration); + + apiEnum = new ApiEnum({ + name, + docComment, + releaseTag, + excerptTokens, + preserveMemberOrder, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + parentApiItem.addMember(apiEnum); + } + + this._processChildDeclarations(astDeclaration, { + ...context, + parentApiItem: apiEnum, + }); + } + + private _processApiEnumMember(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const containerKey: string = ApiEnumMember.getContainerKey(name); + + let apiEnumMember: ApiEnumMember | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiEnumMember; + + if (apiEnumMember === undefined) { + const enumMember: ts.EnumMember = astDeclaration.declaration as ts.EnumMember; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + let initializerTokenRange: IExcerptTokenRange | undefined; + if (enumMember.initializer) { + initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: enumMember.initializer, tokenRange: initializerTokenRange }); + } + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(enumMember); + + apiEnumMember = new ApiEnumMember({ + name, + docComment, + releaseTag, + excerptTokens, + initializerTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiEnumMember); + } + } + + private _processApiFunction(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiFunction.getContainerKey(name, overloadIndex); + + let apiFunction: ApiFunction | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiFunction; + + if (apiFunction === undefined) { + const functionDeclaration: ts.FunctionDeclaration = astDeclaration.declaration as ts.FunctionDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: functionDeclaration.type, tokenRange: returnTypeTokenRange }); + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + functionDeclaration.typeParameters, + ); + + const parameters: IApiParameterOptions[] = this._captureParameters( + nodesToCapture, + functionDeclaration.parameters, + ); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(functionDeclaration); + + apiFunction = new ApiFunction({ + name, + docComment, + releaseTag, + typeParameters, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiFunction); + } + } + + private _processApiIndexSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { parentApiItem } = context; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiIndexSignature.getContainerKey(overloadIndex); + + let apiIndexSignature: ApiIndexSignature | undefined = parentApiItem.tryGetMemberByKey( + containerKey, + ) as ApiIndexSignature; + + if (apiIndexSignature === undefined) { + const indexSignature: ts.IndexSignatureDeclaration = astDeclaration.declaration as ts.IndexSignatureDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: indexSignature.type, tokenRange: returnTypeTokenRange }); + + const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, indexSignature.parameters); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isReadonly: boolean = this._isReadonly(astDeclaration); + const sourceLocation: ISourceLocation = this._getSourceLocation(indexSignature); + + apiIndexSignature = new ApiIndexSignature({ + docComment, + releaseTag, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + isReadonly, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiIndexSignature); + } + } + + private _processApiInterface(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + const containerKey: string = ApiInterface.getContainerKey(name); + + let apiInterface: ApiInterface | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiInterface; + + if (apiInterface === undefined) { + const interfaceDeclaration: ts.InterfaceDeclaration = astDeclaration.declaration as ts.InterfaceDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + interfaceDeclaration.typeParameters, + ); + + const extendsTokenRanges: IExcerptTokenRange[] = []; + + for (const heritageClause of interfaceDeclaration.heritageClauses ?? []) { + if (heritageClause.token === ts.SyntaxKind.ExtendsKeyword) { + for (const heritageType of heritageClause.types) { + const extendsTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + extendsTokenRanges.push(extendsTokenRange); + nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange }); + } + } + } + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(interfaceDeclaration); + + apiInterface = new ApiInterface({ + name, + docComment, + releaseTag, + excerptTokens, + typeParameters, + extendsTokenRanges, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiInterface); + } + + this._processChildDeclarations(astDeclaration, { + ...context, + parentApiItem: apiInterface, + }); + } + + private _processApiMethod(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiMethod.getContainerKey(name, isStatic, overloadIndex); + + let apiMethod: ApiMethod | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiMethod; + + if (apiMethod === undefined) { + const methodDeclaration: ts.MethodDeclaration = astDeclaration.declaration as ts.MethodDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: methodDeclaration.type, tokenRange: returnTypeTokenRange }); + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + methodDeclaration.typeParameters, + ); + + const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodDeclaration.parameters); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + if (releaseTag === ReleaseTag.Internal || releaseTag === ReleaseTag.Alpha) { + return; // trim out items marked as "@internal" or "@alpha" + } + + const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; + const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; + const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0; + const sourceLocation: ISourceLocation = this._getSourceLocation(methodDeclaration); + + apiMethod = new ApiMethod({ + name, + isAbstract, + docComment, + releaseTag, + isProtected, + isStatic, + isOptional, + typeParameters, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiMethod); + } + } + + private _processApiMethodSignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration); + const containerKey: string = ApiMethodSignature.getContainerKey(name, overloadIndex); + + let apiMethodSignature: ApiMethodSignature | undefined = parentApiItem.tryGetMemberByKey( + containerKey, + ) as ApiMethodSignature; + + if (apiMethodSignature === undefined) { + const methodSignature: ts.MethodSignature = astDeclaration.declaration as ts.MethodSignature; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const returnTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: methodSignature.type, tokenRange: returnTypeTokenRange }); + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + methodSignature.typeParameters, + ); + + const parameters: IApiParameterOptions[] = this._captureParameters(nodesToCapture, methodSignature.parameters); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; + const sourceLocation: ISourceLocation = this._getSourceLocation(methodSignature); + + apiMethodSignature = new ApiMethodSignature({ + name, + docComment, + releaseTag, + isOptional, + typeParameters, + parameters, + overloadIndex, + excerptTokens, + returnTypeTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiMethodSignature); + } + } + + private _processApiNamespace(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + const containerKey: string = ApiNamespace.getContainerKey(name); + + let apiNamespace: ApiNamespace | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiNamespace; + + if (apiNamespace === undefined) { + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, []); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(astDeclaration.declaration); + + apiNamespace = new ApiNamespace({ + name, + docComment, + releaseTag, + excerptTokens, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + parentApiItem.addMember(apiNamespace); + } + + this._processChildDeclarations(astDeclaration, { + ...context, + parentApiItem: apiNamespace, + }); + } + + private _processApiProperty(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const isStatic: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Static) !== 0; + const containerKey: string = ApiProperty.getContainerKey(name, isStatic); + + let apiProperty: ApiProperty | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiProperty; + + if (apiProperty === undefined) { + const declaration: ts.Declaration = astDeclaration.declaration; + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + let propertyTypeNode: ts.TypeNode | undefined; + + if (ts.isPropertyDeclaration(declaration) || ts.isGetAccessorDeclaration(declaration)) { + propertyTypeNode = declaration.type; + } + + if (ts.isSetAccessorDeclaration(declaration)) { + // Note that TypeScript always reports an error if a setter does not have exactly one parameter. + propertyTypeNode = declaration.parameters[0]!.type; + } + + nodesToCapture.push({ node: propertyTypeNode, tokenRange: propertyTypeTokenRange }); + + let initializerTokenRange: IExcerptTokenRange | undefined; + if (ts.isPropertyDeclaration(declaration) && declaration.initializer) { + initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: declaration.initializer, tokenRange: initializerTokenRange }); + } + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; + const isProtected: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Protected) !== 0; + const isAbstract: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Abstract) !== 0; + const isReadonly: boolean = this._isReadonly(astDeclaration); + const sourceLocation: ISourceLocation = this._getSourceLocation(declaration); + + apiProperty = new ApiProperty({ + name, + docComment, + releaseTag, + isAbstract, + isProtected, + isStatic, + isOptional, + isReadonly, + excerptTokens, + propertyTypeTokenRange, + initializerTokenRange, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + parentApiItem.addMember(apiProperty); + } else { + // If the property was already declared before (via a merged interface declaration), + // we assume its signature is identical, because the language requires that. + } + } + + private _processApiPropertySignature(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, parentApiItem } = context; + const containerKey: string = ApiPropertySignature.getContainerKey(name); + + let apiPropertySignature: ApiPropertySignature | undefined = parentApiItem.tryGetMemberByKey( + containerKey, + ) as ApiPropertySignature; + + if (apiPropertySignature === undefined) { + const propertySignature: ts.PropertySignature = astDeclaration.declaration as ts.PropertySignature; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const propertyTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: propertySignature.type, tokenRange: propertyTypeTokenRange }); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isOptional: boolean = (astDeclaration.astSymbol.followedSymbol.flags & ts.SymbolFlags.Optional) !== 0; + const isReadonly: boolean = this._isReadonly(astDeclaration); + const sourceLocation: ISourceLocation = this._getSourceLocation(propertySignature); + + apiPropertySignature = new ApiPropertySignature({ + name, + docComment, + releaseTag, + isOptional, + excerptTokens, + propertyTypeTokenRange, + isReadonly, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiPropertySignature); + } else { + // If the property was already declared before (via a merged interface declaration), + // we assume its signature is identical, because the language requires that. + } + } + + private _processApiTypeAlias(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + + const containerKey: string = ApiTypeAlias.getContainerKey(name); + + let apiTypeAlias: ApiTypeAlias | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiTypeAlias; + + if (apiTypeAlias === undefined) { + const typeAliasDeclaration: ts.TypeAliasDeclaration = astDeclaration.declaration as ts.TypeAliasDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const typeParameters: IApiTypeParameterOptions[] = this._captureTypeParameters( + nodesToCapture, + typeAliasDeclaration.typeParameters, + ); + + const typeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: typeAliasDeclaration.type, tokenRange: typeTokenRange }); + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const sourceLocation: ISourceLocation = this._getSourceLocation(typeAliasDeclaration); + + apiTypeAlias = new ApiTypeAlias({ + name, + docComment, + typeParameters, + releaseTag, + excerptTokens, + typeTokenRange, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiTypeAlias); + } + } + + private _processApiVariable(astDeclaration: AstDeclaration, context: IProcessAstEntityContext): void { + const { name, isExported, parentApiItem } = context; + + const containerKey: string = ApiVariable.getContainerKey(name); + + let apiVariable: ApiVariable | undefined = parentApiItem.tryGetMemberByKey(containerKey) as ApiVariable; + + if (apiVariable === undefined) { + const variableDeclaration: ts.VariableDeclaration = astDeclaration.declaration as ts.VariableDeclaration; + + const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; + + const variableTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: variableDeclaration.type, tokenRange: variableTypeTokenRange }); + + let initializerTokenRange: IExcerptTokenRange | undefined; + if (variableDeclaration.initializer) { + initializerTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: variableDeclaration.initializer, tokenRange: initializerTokenRange }); + } + + const excerptTokens: IExcerptToken[] = this._buildExcerptTokens(astDeclaration, nodesToCapture); + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const releaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag; + const isReadonly: boolean = this._isReadonly(astDeclaration); + const sourceLocation: ISourceLocation = this._getSourceLocation(variableDeclaration); + + apiVariable = new ApiVariable({ + name, + docComment, + releaseTag, + excerptTokens, + variableTypeTokenRange, + initializerTokenRange, + isReadonly, + isExported, + fileUrlPath: sourceLocation.sourceFilePath, + fileLine: sourceLocation.sourceFileLine, + fileColumn: sourceLocation.sourceFileColumn, + }); + + parentApiItem.addMember(apiVariable); + } + } + + /** + * @param astDeclaration - The declaration + * @param nodesToCapture - A list of child nodes whose token ranges we want to capture + */ + private _buildExcerptTokens( + astDeclaration: AstDeclaration, + nodesToCapture: IExcerptBuilderNodeToCapture[], + ): IExcerptToken[] { + const excerptTokens: IExcerptToken[] = []; + + // Build the main declaration + ExcerptBuilder.addDeclaration(excerptTokens, astDeclaration, nodesToCapture, this._referenceGenerator); + + const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(astDeclaration); + + // Add any ancillary declarations + for (const ancillaryDeclaration of declarationMetadata.ancillaryDeclarations) { + ExcerptBuilder.addBlankLine(excerptTokens); + ExcerptBuilder.addDeclaration(excerptTokens, ancillaryDeclaration, nodesToCapture, this._referenceGenerator); + } + + return excerptTokens; + } + + private _captureTypeParameters( + nodesToCapture: IExcerptBuilderNodeToCapture[], + typeParameterNodes: ts.NodeArray | undefined, + ): IApiTypeParameterOptions[] { + const typeParameters: IApiTypeParameterOptions[] = []; + if (typeParameterNodes) { + for (const typeParameter of typeParameterNodes) { + const constraintTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: typeParameter.constraint, tokenRange: constraintTokenRange }); + + const defaultTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: typeParameter.default, tokenRange: defaultTypeTokenRange }); + + typeParameters.push({ + typeParameterName: typeParameter.name.getText().trim(), + constraintTokenRange, + defaultTypeTokenRange, + }); + } + } + + return typeParameters; + } + + private _captureParameters( + nodesToCapture: IExcerptBuilderNodeToCapture[], + parameterNodes: ts.NodeArray, + ): IApiParameterOptions[] { + const parameters: IApiParameterOptions[] = []; + for (const parameter of parameterNodes) { + const parameterTypeTokenRange: IExcerptTokenRange = ExcerptBuilder.createEmptyTokenRange(); + nodesToCapture.push({ node: parameter.type, tokenRange: parameterTypeTokenRange }); + parameters.push({ + parameterName: parameter.name.getText().trim(), + parameterTypeTokenRange, + isOptional: this._collector.typeChecker.isOptionalParameter(parameter), + isRest: Boolean(parameter.dotDotDotToken), + }); + } + + return parameters; + } + + private _isReadonly(astDeclaration: AstDeclaration): boolean { + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.IndexSignature: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.VariableDeclaration: { + const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); + const docComment: tsdoc.DocComment | undefined = apiItemMetadata.tsdocComment; + const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(astDeclaration); + + const hasReadonlyModifier: boolean = (astDeclaration.modifierFlags & ts.ModifierFlags.Readonly) !== 0; + const hasReadonlyDocTag = Boolean(docComment?.modifierTagSet?.hasTagName('@readonly')); + const isGetterWithNoSetter: boolean = + ts.isGetAccessorDeclaration(astDeclaration.declaration) && + declarationMetadata.ancillaryDeclarations.length === 0; + const isVarConst: boolean = + ts.isVariableDeclaration(astDeclaration.declaration) && + TypeScriptInternals.isVarConst(astDeclaration.declaration); + + return hasReadonlyModifier || hasReadonlyDocTag || isGetterWithNoSetter || isVarConst; + } + + default: { + // Readonly-ness does not make sense for any other declaration kind. + return false; + } + } + } + + private _getSourceLocation(declaration: ts.Declaration): ISourceLocation { + const sourceFile: ts.SourceFile = declaration.getSourceFile(); + const sourceLocation: ISourceLocation = this._collector.sourceMapper.getSourceLocation({ + sourceFile, + pos: declaration.getStart(sourceFile, false), + }); + + sourceLocation.sourceFilePath = Path.convertToSlashes( + path.relative(this._collector.extractorConfig.projectFolder, sourceLocation.sourceFilePath), + ); + return sourceLocation; + } +} diff --git a/packages/api-extractor/src/generators/ApiReportGenerator.ts b/packages/api-extractor/src/generators/ApiReportGenerator.ts new file mode 100644 index 000000000..bae689663 --- /dev/null +++ b/packages/api-extractor/src/generators/ApiReportGenerator.ts @@ -0,0 +1,539 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable @typescript-eslint/require-array-sort-compare */ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ReleaseTag, releaseTagGetTagName } from '@discordjs/api-extractor-model'; +import { Text, InternalError } from '@rushstack/node-core-library'; +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 { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js'; +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 { ApiItemMetadata } from '../collector/ApiItemMetadata.js'; +import { Collector } from '../collector/Collector.js'; +import type { CollectorEntity } from '../collector/CollectorEntity.js'; +import { DtsEmitHelpers } from './DtsEmitHelpers.js'; +import { IndentedWriter } from './IndentedWriter.js'; + +export class ApiReportGenerator { + private static _trimSpacesRegExp: RegExp = / +$/gm; + + /** + * Compares the contents of two API files that were created using ApiFileGenerator, + * and returns true if they are equivalent. Note that these files are not normally edited + * by a human; the "equivalence" comparison here is intended to ignore spurious changes that + * might be introduced by a tool, e.g. Git newline normalization or an editor that strips + * whitespace when saving. + */ + public static areEquivalentApiFileContents(actualFileContent: string, expectedFileContent: string): boolean { + // NOTE: "\s" also matches "\r" and "\n" + const normalizedActual: string = actualFileContent.replaceAll(/\s+/g, ' '); + const normalizedExpected: string = expectedFileContent.replaceAll(/\s+/g, ' '); + return normalizedActual === normalizedExpected; + } + + public static generateReviewFileContent(collector: Collector): string { + const writer: IndentedWriter = new IndentedWriter(); + writer.trimLeadingSpaces = true; + + 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'), + ); + + // Write the opening delimiter for the Markdown code fence + writer.writeLine('```ts\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(`/// `); + } + + for (const libDirectiveReference of Array.from(collector.dtsLibReferenceDirectives).sort()) { + writer.writeLine(`/// `); + } + + writer.ensureSkippedLine(); + + // Emit the imports + for (const entity of collector.entities) { + if (entity.astEntity instanceof AstImport) { + DtsEmitHelpers.emitImport(writer, entity, entity.astEntity); + } + } + + writer.ensureSkippedLine(); + + // 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; + } + const exportsToEmit: Map = new Map(); + + for (const exportName of entity.exportNames) { + if (!entity.shouldInlineExport) { + exportsToEmit.set(exportName, { exportName, associatedMessages: [] }); + } + } + + 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); + } + + 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), + ); + } + + // 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) { + // 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 { ... }" + } + + // 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), + ); + } + } + + if (collector.workingPackage.tsdocComment === undefined) { + writer.ensureSkippedLine(); + ApiReportGenerator._writeLineAsComments(writer, '(No @packageDocumentation comment for this package)'); + } + + // Write the closing delimiter for the Markdown code fence + writer.ensureSkippedLine(); + writer.writeLine('```'); + + // Remove any trailing spaces + return writer.toString().replace(ApiReportGenerator._trimSpacesRegExp, ''); + } + + /** + * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. + */ + private static _modifySpan( + collector: Collector, + span: Span, + entity: CollectorEntity, + astDeclaration: AstDeclaration, + insideTypeLiteral: boolean, + ): void { + // Should we process this declaration at all? + + if ((astDeclaration.modifierFlags & ts.ModifierFlags.Private) !== 0) { + span.modification.skipAll(); + return; + } + + const previousSpan: Span | undefined = span.previousSibling; + + let recurseChildren = true; + let sortChildren = false; + + switch (span.kind) { + case ts.SyntaxKind.JSDocComment: + span.modification.skipAll(); + // For now, we don't transform JSDoc comment nodes at all + recurseChildren = false; + break; + + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.DeclareKeyword: + // Delete any explicit "export" or "declare" keywords -- we will re-add them below + span.modification.skipAll(); + break; + + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.FunctionKeyword: + // Replace the stuff we possibly deleted above + let replacedModifiers = ''; + + if (entity.shouldInlineExport) { + replacedModifiers = 'export ' + replacedModifiers; + } + + if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { + // If there is a previous span of type SyntaxList, then apply it before any other modifiers + // (e.g. "abstract") that appear there. + previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; + } else { + // Otherwise just stick it in front of this span + span.modification.prefix = replacedModifiers + span.modification.prefix; + } + + break; + + case ts.SyntaxKind.SyntaxList: + if ( + span.parent && + (AstDeclaration.isSupportedSyntaxKind(span.parent.kind) || span.parent.kind === ts.SyntaxKind.ModuleBlock) + ) { + // If the immediate parent is an API declaration, and the immediate children are API declarations, + // then sort the children alphabetically + // Namespaces are special because their chain goes ModuleDeclaration -> ModuleBlock -> SyntaxList + sortChildren = true; + } + + break; + + case ts.SyntaxKind.VariableDeclaration: + if (!span.parent) { + // The VariableDeclaration node is part of a VariableDeclarationList, however + // the Entry.followedSymbol points to the VariableDeclaration part because + // multiple definitions might share the same VariableDeclarationList. + // + // Since we are emitting a separate declaration for each one, we need to look upwards + // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList + // content (e.g. "var" from "var x=1, y=2"). + const list: ts.VariableDeclarationList | undefined = TypeScriptHelpers.matchAncestor(span.node, [ + ts.SyntaxKind.VariableDeclarationList, + ts.SyntaxKind.VariableDeclaration, + ]); + if (!list) { + // This should not happen unless the compiler API changes somehow + throw new InternalError('Unsupported variable declaration'); + } + + const listPrefix: string = list.getSourceFile().text.slice(list.getStart(), list.declarations[0]!.getStart()); + span.modification.prefix = listPrefix + span.modification.prefix; + span.modification.suffix = ';'; + + if (entity.shouldInlineExport) { + span.modification.prefix = 'export ' + span.modification.prefix; + } + } + + break; + + case ts.SyntaxKind.Identifier: + const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityForNode(span.node as ts.Identifier); + + if (referencedEntity) { + if (!referencedEntity.nameForEmit) { + // This should never happen + throw new InternalError('referencedEntry.nameForEmit is undefined'); + } + + span.modification.prefix = referencedEntity.nameForEmit; + // For debugging: + // span.modification.prefix += '/*R=FIX*/'; + } else { + // For debugging: + // span.modification.prefix += '/*R=KEEP*/'; + } + + break; + + case ts.SyntaxKind.TypeLiteral: + // eslint-disable-next-line no-param-reassign + insideTypeLiteral = true; + break; + + case ts.SyntaxKind.ImportType: + DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => { + ApiReportGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, insideTypeLiteral); + }); + break; + + default: + break; + } + + if (recurseChildren) { + for (const child of span.children) { + let childAstDeclaration: AstDeclaration = astDeclaration; + + if (AstDeclaration.isSupportedSyntaxKind(child.kind)) { + childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); + + if (sortChildren) { + span.modification.sortChildren = true; + child.modification.sortKey = Collector.getSortKeyIgnoringUnderscore( + childAstDeclaration.astSymbol.localName, + ); + } + + if (!insideTypeLiteral) { + const messagesToReport: ExtractorMessage[] = + collector.messageRouter.fetchAssociatedMessagesForReviewFile(childAstDeclaration); + const aedocSynopsis: string = ApiReportGenerator._getAedocSynopsis( + collector, + childAstDeclaration, + messagesToReport, + ); + + child.modification.prefix = aedocSynopsis + child.modification.prefix; + } + } + + ApiReportGenerator._modifySpan(collector, child, entity, childAstDeclaration, insideTypeLiteral); + } + } + } + + /** + * For declarations marked as `@preapproved`, this is used instead of _modifySpan(). + */ + private static _modifySpanForPreapproved(span: Span): void { + // Match something like this: + // + // ClassDeclaration: + // SyntaxList: + // ExportKeyword: pre=[export] sep=[ ] + // DeclareKeyword: pre=[declare] sep=[ ] + // ClassKeyword: pre=[class] sep=[ ] + // Identifier: pre=[_PreapprovedClass] sep=[ ] + // FirstPunctuation: pre=[{] sep=[\n\n ] + // SyntaxList: + // ... + // CloseBraceToken: pre=[}] + // + // or this: + // ModuleDeclaration: + // SyntaxList: + // ExportKeyword: pre=[export] sep=[ ] + // DeclareKeyword: pre=[declare] sep=[ ] + // NamespaceKeyword: pre=[namespace] sep=[ ] + // Identifier: pre=[_PreapprovedNamespace] sep=[ ] + // ModuleBlock: + // FirstPunctuation: pre=[{] sep=[\n\n ] + // SyntaxList: + // ... + // CloseBraceToken: pre=[}] + // + // And reduce it to something like this: + // + // // @internal (undocumented) + // class _PreapprovedClass { /* (preapproved) */ } + // + + let skipRest = false; + for (const child of span.children) { + if (skipRest || child.kind === ts.SyntaxKind.SyntaxList || child.kind === ts.SyntaxKind.JSDocComment) { + child.modification.skipAll(); + } + + if (child.kind === ts.SyntaxKind.Identifier) { + skipRest = true; + child.modification.omitSeparatorAfter = true; + child.modification.suffix = ' { /* (preapproved) */ }'; + } + } + } + + /** + * Writes a synopsis of the AEDoc comments, which indicates the release tag, + * whether the item has been documented, and any warnings that were detected + * by the analysis. + */ + private static _getAedocSynopsis( + collector: Collector, + astDeclaration: AstDeclaration, + messagesToReport: ExtractorMessage[], + ): string { + const writer: IndentedWriter = new IndentedWriter(); + + for (const message of messagesToReport) { + ApiReportGenerator._writeLineAsComments(writer, 'Warning: ' + message.formatMessageWithoutLocation()); + } + + if (!collector.isAncillaryDeclaration(astDeclaration)) { + const footerParts: string[] = []; + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + if (!apiItemMetadata.releaseTagSameAsParent && apiItemMetadata.effectiveReleaseTag !== ReleaseTag.None) { + footerParts.push(releaseTagGetTagName(apiItemMetadata.effectiveReleaseTag)); + } + + if (apiItemMetadata.isSealed) { + footerParts.push('@sealed'); + } + + if (apiItemMetadata.isVirtual) { + footerParts.push('@virtual'); + } + + if (apiItemMetadata.isOverride) { + footerParts.push('@override'); + } + + if (apiItemMetadata.isEventProperty) { + footerParts.push('@eventProperty'); + } + + if (apiItemMetadata.tsdocComment?.deprecatedBlock) { + footerParts.push('@deprecated'); + } + + if (apiItemMetadata.undocumented) { + footerParts.push('(undocumented)'); + + collector.messageRouter.addAnalyzerIssue( + ExtractorMessageId.Undocumented, + `Missing documentation for "${astDeclaration.astSymbol.localName}".`, + astDeclaration, + ); + } + + if (footerParts.length > 0) { + if (messagesToReport.length > 0) { + ApiReportGenerator._writeLineAsComments(writer, ''); // skip a line after the warnings + } + + ApiReportGenerator._writeLineAsComments(writer, footerParts.join(' ')); + } + } + + return writer.toString(); + } + + private static _writeLineAsComments(writer: IndentedWriter, line: string): void { + const lines: string[] = Text.convertToLf(line).split('\n'); + for (const realLine of lines) { + writer.write('// '); + writer.write(realLine); + writer.writeLine(); + } + } +} diff --git a/packages/api-extractor/src/generators/DeclarationReferenceGenerator.ts b/packages/api-extractor/src/generators/DeclarationReferenceGenerator.ts new file mode 100644 index 000000000..6a8924329 --- /dev/null +++ b/packages/api-extractor/src/generators/DeclarationReferenceGenerator.ts @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { Navigation, Meaning } from '@discordjs/api-extractor-model'; +import { + DeclarationReference, + ModuleSource, + GlobalSource, +} from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; +import { type INodePackageJson, InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js'; +import { TypeScriptInternals } from '../analyzer/TypeScriptInternals.js'; +import type { Collector } from '../collector/Collector.js'; +import type { CollectorEntity } from '../collector/CollectorEntity.js'; + +export class DeclarationReferenceGenerator { + public static readonly unknownReference: string = '?'; + + private readonly _collector: Collector; + + public constructor(collector: Collector) { + this._collector = collector; + } + + /** + * Gets the UID for a TypeScript Identifier that references a type. + */ + public getDeclarationReferenceForIdentifier(node: ts.Identifier): DeclarationReference | undefined { + const symbol: ts.Symbol | undefined = this._collector.typeChecker.getSymbolAtLocation(node); + if (symbol !== undefined) { + const isExpression: boolean = DeclarationReferenceGenerator._isInExpressionContext(node); + return ( + this.getDeclarationReferenceForSymbol(symbol, isExpression ? ts.SymbolFlags.Value : ts.SymbolFlags.Type) ?? + this.getDeclarationReferenceForSymbol(symbol, isExpression ? ts.SymbolFlags.Type : ts.SymbolFlags.Value) ?? + this.getDeclarationReferenceForSymbol(symbol, ts.SymbolFlags.Namespace) + ); + } + + return undefined; + } + + /** + * Gets the DeclarationReference for a TypeScript Symbol for a given meaning. + */ + public getDeclarationReferenceForSymbol( + symbol: ts.Symbol, + meaning: ts.SymbolFlags, + ): DeclarationReference | undefined { + return this._symbolToDeclarationReference(symbol, meaning, /* includeModuleSymbols*/ false); + } + + private static _isInExpressionContext(node: ts.Node): boolean { + switch (node.parent.kind) { + case ts.SyntaxKind.TypeQuery: + case ts.SyntaxKind.ComputedPropertyName: + return true; + case ts.SyntaxKind.QualifiedName: + return DeclarationReferenceGenerator._isInExpressionContext(node.parent); + default: + return false; + } + } + + private static _isExternalModuleSymbol(symbol: ts.Symbol): boolean { + return ( + Boolean(symbol.flags & ts.SymbolFlags.ValueModule) && + symbol.valueDeclaration !== undefined && + ts.isSourceFile(symbol.valueDeclaration) + ); + } + + private static _isSameSymbol(left: ts.Symbol | undefined, right: ts.Symbol): boolean { + return ( + left === right || + Boolean(left?.valueDeclaration && right.valueDeclaration && left.valueDeclaration === right.valueDeclaration) + ); + } + + private _getNavigationToSymbol(symbol: ts.Symbol): Navigation { + const declaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(symbol); + const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile(); + const parent: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol); + + // If it's global or from an external library, then use either Members or Exports. It's not possible for + // global symbols or external library symbols to be Locals. + const isGlobal: boolean = Boolean(sourceFile) && !ts.isExternalModule(sourceFile!); + const isFromExternalLibrary: boolean = + Boolean(sourceFile) && this._collector.program.isSourceFileFromExternalLibrary(sourceFile!); + if (isGlobal || isFromExternalLibrary) { + if ( + parent?.members && + DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol) + ) { + return Navigation.Members; + } + + return Navigation.Exports; + } + + // Otherwise, this symbol is from the current package. If we've found an associated consumable + // `CollectorEntity`, then use Exports. We use `consumable` here instead of `exported` because + // if the symbol is exported from a non-consumable `AstNamespaceImport`, we don't want to use + // Exports. We should use Locals instead. + const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(symbol); + if (entity?.consumable) { + return Navigation.Exports; + } + + // If its parent symbol is not a source file, then use either Exports or Members. If the parent symbol + // is a source file, but it wasn't exported from the package entry point (in the check above), then the + // symbol is a local, so fall through below. + if (parent && !DeclarationReferenceGenerator._isExternalModuleSymbol(parent)) { + if ( + parent.members && + DeclarationReferenceGenerator._isSameSymbol(parent.members.get(symbol.escapedName), symbol) + ) { + return Navigation.Members; + } + + return Navigation.Exports; + } + + // Otherwise, we have a local symbol, so use a Locals navigation. These are either: + // + // 1. Symbols that are exported from a file module but not the package entry point. + // 2. Symbols that are not exported from their parent module. + return Navigation.Locals; + } + + private static _getMeaningOfSymbol(symbol: ts.Symbol, meaning: ts.SymbolFlags): Meaning | undefined { + if (symbol.flags & meaning & ts.SymbolFlags.Class) { + return Meaning.Class; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Enum) { + return Meaning.Enum; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Interface) { + return Meaning.Interface; + } + + if (symbol.flags & meaning & ts.SymbolFlags.TypeAlias) { + return Meaning.TypeAlias; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Function) { + return Meaning.Function; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Variable) { + return Meaning.Variable; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Module) { + return Meaning.Namespace; + } + + if (symbol.flags & meaning & ts.SymbolFlags.ClassMember) { + return Meaning.Member; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Constructor) { + return Meaning.Constructor; + } + + if (symbol.flags & meaning & ts.SymbolFlags.EnumMember) { + return Meaning.Member; + } + + if (symbol.flags & meaning & ts.SymbolFlags.Signature) { + if (symbol.escapedName === ts.InternalSymbolName.Call) { + return Meaning.CallSignature; + } + + if (symbol.escapedName === ts.InternalSymbolName.New) { + return Meaning.ConstructSignature; + } + + if (symbol.escapedName === ts.InternalSymbolName.Index) { + return Meaning.IndexSignature; + } + } + + if (symbol.flags & meaning & ts.SymbolFlags.TypeParameter) { + // This should have already been handled in `getDeclarationReferenceOfSymbol`. + throw new InternalError('Not supported.'); + } + + return undefined; + } + + private _symbolToDeclarationReference( + symbol: ts.Symbol, + meaning: ts.SymbolFlags, + includeModuleSymbols: boolean, + ): DeclarationReference | undefined { + const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol); + const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile(); + + let followedSymbol: ts.Symbol = symbol; + if (followedSymbol.flags & ts.SymbolFlags.ExportValue) { + followedSymbol = this._collector.typeChecker.getExportSymbolOfSymbol(followedSymbol); + } + + if (followedSymbol.flags & ts.SymbolFlags.Alias) { + followedSymbol = this._collector.typeChecker.getAliasedSymbol(followedSymbol); + + // Without this logic, we end up following the symbol `ns` in `import * as ns from './file'` to + // the actual file `file.ts`. We don't want to do this, so revert to the original symbol. + if (followedSymbol.flags & ts.SymbolFlags.ValueModule) { + followedSymbol = symbol; + } + } + + if (DeclarationReferenceGenerator._isExternalModuleSymbol(followedSymbol)) { + if (!includeModuleSymbols) { + return undefined; + } + + return new DeclarationReference(this._sourceFileToModuleSource(sourceFile)); + } + + // Do not generate a declaration reference for a type parameter. + if (followedSymbol.flags & ts.SymbolFlags.TypeParameter) { + return undefined; + } + + let parentRef: DeclarationReference | undefined = this._getParentReference(followedSymbol); + if (!parentRef) { + return undefined; + } + + let localName: string = followedSymbol.name; + const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(followedSymbol); + if (entity?.nameForEmit) { + localName = entity.nameForEmit; + } + + if (followedSymbol.escapedName === ts.InternalSymbolName.Constructor) { + localName = 'constructor'; + } else { + const wellKnownName: string | undefined = TypeScriptHelpers.tryDecodeWellKnownSymbolName( + followedSymbol.escapedName, + ); + if (wellKnownName) { + // TypeScript binds well-known ECMAScript symbols like 'Symbol.iterator' as '__@iterator'. + // This converts a string like '__@iterator' into the property name '[Symbol.iterator]'. + localName = wellKnownName; + } else if (TypeScriptHelpers.isUniqueSymbolName(followedSymbol.escapedName)) { + for (const decl of followedSymbol.declarations ?? []) { + const declName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(decl); + if (declName && ts.isComputedPropertyName(declName)) { + const lateName: string | undefined = TypeScriptHelpers.tryGetLateBoundName(declName); + if (lateName !== undefined) { + localName = lateName; + break; + } + } + } + } + } + + const navigation: Navigation = this._getNavigationToSymbol(followedSymbol); + + // If the symbol is a global, ensure the source is global. + if (sourceFile && !ts.isExternalModule(sourceFile) && parentRef.source !== GlobalSource.instance) { + parentRef = new DeclarationReference(GlobalSource.instance); + } + + return parentRef + .addNavigationStep(navigation as any, localName) + .withMeaning(DeclarationReferenceGenerator._getMeaningOfSymbol(followedSymbol, meaning) as any); + } + + private _getParentReference(symbol: ts.Symbol): DeclarationReference | undefined { + const declaration: ts.Node | undefined = TypeScriptHelpers.tryGetADeclaration(symbol); + const sourceFile: ts.SourceFile | undefined = declaration?.getSourceFile(); + + // Note that it's possible for a symbol to be exported from an entry point as well as one or more + // namespaces. In that case, it's not clear what to choose as its parent. Today's logic is neither + // perfect nor particularly stable to API items being renamed and shuffled around. + const entity: CollectorEntity | undefined = this._collector.tryGetEntityForSymbol(symbol); + if (entity) { + if (entity.exportedFromEntryPoint) { + return new DeclarationReference(this._sourceFileToModuleSource(sourceFile)); + } + + const firstExportingConsumableParent: CollectorEntity | undefined = entity.getFirstExportingConsumableParent(); + if (firstExportingConsumableParent && firstExportingConsumableParent.astEntity instanceof AstNamespaceImport) { + const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration( + firstExportingConsumableParent.astEntity.declaration, + this._collector.typeChecker, + ); + if (parentSymbol) { + return this._symbolToDeclarationReference(parentSymbol, parentSymbol.flags, /* includeModuleSymbols*/ true); + } + } + } + + // Next, try to find a parent symbol via the symbol tree. + const parentSymbol: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(symbol); + if (parentSymbol) { + return this._symbolToDeclarationReference(parentSymbol, parentSymbol.flags, /* includeModuleSymbols*/ true); + } + + // If that doesn't work, try to find a parent symbol via the node tree. As far as we can tell, + // this logic is only needed for local symbols within namespaces. For example: + // + // ``` + // export namespace n { + // type SomeType = number; + // export function someFunction(): SomeType { return 5; } + // } + // ``` + // + // In the example above, `SomeType` doesn't have a parent symbol per the TS internal API above, + // but its reference still needs to be qualified with the parent reference for `n`. + const grandParent: ts.Node | undefined = declaration?.parent?.parent; + if (grandParent && ts.isModuleDeclaration(grandParent)) { + const grandParentSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration( + grandParent, + this._collector.typeChecker, + ); + if (grandParentSymbol) { + return this._symbolToDeclarationReference( + grandParentSymbol, + grandParentSymbol.flags, + /* includeModuleSymbols*/ true, + ); + } + } + + // At this point, we have a local symbol in a module. + if (sourceFile && ts.isExternalModule(sourceFile)) { + return new DeclarationReference(this._sourceFileToModuleSource(sourceFile)); + } else { + return new DeclarationReference(GlobalSource.instance); + } + } + + private _getPackageName(sourceFile: ts.SourceFile): string { + if (this._collector.program.isSourceFileFromExternalLibrary(sourceFile)) { + const packageJson: INodePackageJson | undefined = this._collector.packageJsonLookup.tryLoadNodePackageJsonFor( + sourceFile.fileName, + ); + + if (packageJson?.name) { + return packageJson.name; + } + + return DeclarationReferenceGenerator.unknownReference; + } + + return this._collector.workingPackage.name; + } + + private _sourceFileToModuleSource(sourceFile: ts.SourceFile | undefined): GlobalSource | ModuleSource { + if (sourceFile && ts.isExternalModule(sourceFile)) { + const packageName: string = this._getPackageName(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); + } else { + return new ModuleSource(packageName); + } + } + + return GlobalSource.instance; + } +} diff --git a/packages/api-extractor/src/generators/DtsEmitHelpers.ts b/packages/api-extractor/src/generators/DtsEmitHelpers.ts new file mode 100644 index 000000000..9d6a048cc --- /dev/null +++ b/packages/api-extractor/src/generators/DtsEmitHelpers.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import * as ts from 'typescript'; +import { AstDeclaration } from '../analyzer/AstDeclaration.js'; +import { AstImport, AstImportKind } from '../analyzer/AstImport.js'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js'; +import type { Span } from '../analyzer/Span.js'; +import type { Collector } from '../collector/Collector.js'; +import type { CollectorEntity } from '../collector/CollectorEntity.js'; +import type { IndentedWriter } from './IndentedWriter.js'; + +/** + * Some common code shared between DtsRollupGenerator and ApiReportGenerator. + */ +export class DtsEmitHelpers { + public static emitImport(writer: IndentedWriter, collectorEntity: CollectorEntity, astImport: AstImport): void { + const importPrefix: string = astImport.isTypeOnlyEverywhere ? 'import type' : 'import'; + + switch (astImport.importKind) { + case AstImportKind.DefaultImport: + if (collectorEntity.nameForEmit === astImport.exportName) { + writer.write(`${importPrefix} ${astImport.exportName}`); + } else { + writer.write(`${importPrefix} { default as ${collectorEntity.nameForEmit} }`); + } + + writer.writeLine(` from '${astImport.modulePath}';`); + break; + case AstImportKind.NamedImport: + if (collectorEntity.nameForEmit === astImport.exportName) { + writer.write(`${importPrefix} { ${astImport.exportName} }`); + } else { + writer.write(`${importPrefix} { ${astImport.exportName} as ${collectorEntity.nameForEmit} }`); + } + + writer.writeLine(` from '${astImport.modulePath}';`); + break; + case AstImportKind.StarImport: + writer.writeLine(`${importPrefix} * as ${collectorEntity.nameForEmit} from '${astImport.modulePath}';`); + break; + case AstImportKind.EqualsImport: + writer.writeLine(`${importPrefix} ${collectorEntity.nameForEmit} = require('${astImport.modulePath}');`); + break; + case AstImportKind.ImportType: + if (astImport.exportName) { + const topExportName: string = astImport.exportName.split('.')[0]!; + if (collectorEntity.nameForEmit === topExportName) { + writer.write(`${importPrefix} { ${topExportName} }`); + } else { + writer.write(`${importPrefix} { ${topExportName} as ${collectorEntity.nameForEmit} }`); + } + + writer.writeLine(` from '${astImport.modulePath}';`); + } else { + writer.writeLine(`${importPrefix} * as ${collectorEntity.nameForEmit} from '${astImport.modulePath}';`); + } + + break; + default: + throw new InternalError('Unimplemented AstImportKind'); + } + } + + public static emitNamedExport(writer: IndentedWriter, exportName: string, collectorEntity: CollectorEntity): void { + if (exportName === ts.InternalSymbolName.Default) { + writer.writeLine(`export default ${collectorEntity.nameForEmit};`); + } else if (collectorEntity.nameForEmit === exportName) { + writer.writeLine(`export { ${exportName} }`); + } else { + writer.writeLine(`export { ${collectorEntity.nameForEmit} as ${exportName} }`); + } + } + + public static emitStarExports(writer: IndentedWriter, collector: Collector): void { + if (collector.starExportedExternalModulePaths.length > 0) { + writer.writeLine(); + for (const starExportedExternalModulePath of collector.starExportedExternalModulePaths) { + writer.writeLine(`export * from "${starExportedExternalModulePath}";`); + } + } + } + + public static modifyImportTypeSpan( + collector: Collector, + span: Span, + astDeclaration: AstDeclaration, + modifyNestedSpan: (childSpan: Span, childAstDeclaration: AstDeclaration) => void, + ): void { + const node: ts.ImportTypeNode = span.node as ts.ImportTypeNode; + const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityForNode(node); + + if (referencedEntity) { + if (!referencedEntity.nameForEmit) { + // This should never happen + + throw new InternalError('referencedEntry.nameForEmit is undefined'); + } + + let typeArgumentsText = ''; + + if (node.typeArguments && node.typeArguments.length > 0) { + // Type arguments have to be processed and written to the document + const lessThanTokenPos: number = span.children.findIndex( + (childSpan) => childSpan.node.kind === ts.SyntaxKind.LessThanToken, + ); + const greaterThanTokenPos: number = span.children.findIndex( + (childSpan) => childSpan.node.kind === ts.SyntaxKind.GreaterThanToken, + ); + + if (lessThanTokenPos < 0 || greaterThanTokenPos <= lessThanTokenPos) { + throw new InternalError( + `Invalid type arguments: ${node.getText()}\n` + SourceFileLocationFormatter.formatDeclaration(node), + ); + } + + const typeArgumentsSpans: Span[] = span.children.slice(lessThanTokenPos + 1, greaterThanTokenPos); + + // Apply modifications to Span elements of typeArguments + for (const childSpan of typeArgumentsSpans) { + const childAstDeclaration: AstDeclaration = AstDeclaration.isSupportedSyntaxKind(childSpan.kind) + ? collector.astSymbolTable.getChildAstDeclarationByNode(childSpan.node, astDeclaration) + : astDeclaration; + + modifyNestedSpan(childSpan, childAstDeclaration); + } + + const typeArgumentsStrings: string[] = typeArgumentsSpans.map((childSpan) => childSpan.getModifiedText()); + typeArgumentsText = `<${typeArgumentsStrings.join(', ')}>`; + } + + const separatorAfter: string = /(?\s*)$/.exec(span.getText())?.groups?.separator ?? ''; + + if ( + referencedEntity.astEntity instanceof AstImport && + referencedEntity.astEntity.importKind === AstImportKind.ImportType && + referencedEntity.astEntity.exportName + ) { + // For an ImportType with a namespace chain, only the top namespace is imported. + // Must add the original nested qualifiers to the rolled up import. + const qualifiersText: string = node.qualifier?.getText() ?? ''; + const nestedQualifiersStart: number = qualifiersText.indexOf('.'); + // Including the leading "." + const nestedQualifiersText: string = + nestedQualifiersStart >= 0 ? qualifiersText.slice(Math.max(0, nestedQualifiersStart)) : ''; + + const replacement = `${referencedEntity.nameForEmit}${nestedQualifiersText}${typeArgumentsText}${separatorAfter}`; + + span.modification.skipAll(); + span.modification.prefix = replacement; + } else { + // Replace with internal symbol or AstImport + span.modification.skipAll(); + span.modification.prefix = `${referencedEntity.nameForEmit}${typeArgumentsText}${separatorAfter}`; + } + } + } +} diff --git a/packages/api-extractor/src/generators/DtsRollupGenerator.ts b/packages/api-extractor/src/generators/DtsRollupGenerator.ts new file mode 100644 index 000000000..3089848e8 --- /dev/null +++ b/packages/api-extractor/src/generators/DtsRollupGenerator.ts @@ -0,0 +1,473 @@ +/* eslint-disable no-case-declarations */ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ReleaseTag } from '@discordjs/api-extractor-model'; +import { FileSystem, type NewlineKind, InternalError } from '@rushstack/node-core-library'; +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 { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js'; +import { AstSymbol } from '../analyzer/AstSymbol.js'; +import { SourceFileLocationFormatter } from '../analyzer/SourceFileLocationFormatter.js'; +import { IndentDocCommentScope, Span, type SpanModification } from '../analyzer/Span.js'; +import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js'; +import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js'; +import type { Collector } from '../collector/Collector.js'; +import type { CollectorEntity } from '../collector/CollectorEntity.js'; +import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js'; +import type { SymbolMetadata } from '../collector/SymbolMetadata.js'; +import { DtsEmitHelpers } from './DtsEmitHelpers.js'; +import { IndentedWriter } from './IndentedWriter.js'; + +/** + * Used with DtsRollupGenerator.writeTypingsFile() + */ +export enum DtsRollupKind { + /** + * Generate a *.d.ts file for an internal release, or for the trimming=false mode. + * This output file will contain all definitions that are reachable from the entry point. + */ + InternalRelease, + + /** + * Generate a *.d.ts file for a preview release. + * This output file will contain all definitions that are reachable from the entry point, + * except definitions marked as \@internal. + */ + AlphaRelease, + + /** + * Generate a *.d.ts file for a preview release. + * This output file will contain all definitions that are reachable from the entry point, + * except definitions marked as \@alpha or \@internal. + */ + BetaRelease, + + /** + * Generate a *.d.ts file for a public release. + * This output file will contain all definitions that are reachable from the entry point, + * except definitions marked as \@beta, \@alpha, or \@internal. + */ + PublicRelease, +} + +export class DtsRollupGenerator { + /** + * Generates the typings file and writes it to disk. + * + * @param collector - The Collector + * @param dtsFilename - The *.d.ts output filename + */ + public static writeTypingsFile( + collector: Collector, + dtsFilename: string, + dtsKind: DtsRollupKind, + newlineKind: NewlineKind, + ): void { + const writer: IndentedWriter = new IndentedWriter(); + writer.trimLeadingSpaces = true; + + DtsRollupGenerator._generateTypingsFileContent(collector, writer, dtsKind); + + FileSystem.writeFile(dtsFilename, writer.toString(), { + convertLineEndings: newlineKind, + ensureFolderExists: true, + }); + } + + private static _generateTypingsFileContent( + collector: Collector, + writer: IndentedWriter, + dtsKind: DtsRollupKind, + ): void { + // Emit the @packageDocumentation comment at the top of the file + if (collector.workingPackage.tsdocParserContext) { + writer.trimLeadingSpaces = false; + writer.writeLine(collector.workingPackage.tsdocParserContext.sourceRange.toString()); + writer.trimLeadingSpaces = true; + writer.ensureSkippedLine(); + } + + // Emit the triple slash directives + for (const typeDirectiveReference of collector.dtsTypeReferenceDirectives) { + // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 + writer.writeLine(`/// `); + } + + for (const libDirectiveReference of collector.dtsLibReferenceDirectives) { + writer.writeLine(`/// `); + } + + writer.ensureSkippedLine(); + + // Emit the imports + for (const entity of collector.entities) { + if (entity.astEntity instanceof AstImport) { + const astImport: AstImport = entity.astEntity; + + // For example, if the imported API comes from an external package that supports AEDoc, + // and it was marked as `@internal`, then don't emit it. + const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astImport); + const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata + ? symbolMetadata.maxEffectiveReleaseTag + : ReleaseTag.None; + + if (this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, dtsKind)) { + DtsEmitHelpers.emitImport(writer, entity, astImport); + } + } + } + + writer.ensureSkippedLine(); + + // Emit the regular declarations + for (const entity of collector.entities) { + const astEntity: AstEntity = entity.astEntity; + const symbolMetadata: SymbolMetadata | undefined = collector.tryFetchMetadataForAstEntity(astEntity); + const maxEffectiveReleaseTag: ReleaseTag = symbolMetadata + ? symbolMetadata.maxEffectiveReleaseTag + : ReleaseTag.None; + + if (!this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, dtsKind)) { + if (!collector.extractorConfig.omitTrimmingComments) { + writer.ensureSkippedLine(); + writer.writeLine(`/* Excluded from this release type: ${entity.nameForEmit} */`); + } + + continue; + } + + if (astEntity instanceof AstSymbol) { + // Emit all the declarations for this entry + for (const astDeclaration of astEntity.astDeclarations || []) { + const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + + if (this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, dtsKind)) { + const span: Span = new Span(astDeclaration.declaration); + DtsRollupGenerator._modifySpan(collector, span, entity, astDeclaration, dtsKind); + writer.ensureSkippedLine(); + span.writeModifiedText(writer); + writer.ensureNewLine(); + } else if (!collector.extractorConfig.omitTrimmingComments) { + writer.ensureSkippedLine(); + writer.writeLine(`/* Excluded declaration from this release type: ${entity.nameForEmit} */`); + } + } + } + + 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 start export, which is not supported:\n` + + SourceFileLocationFormatter.formatDeclaration(astEntity.declaration), + ); + } + + // 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(); + if (entity.shouldInlineExport) { + writer.write('export '); + } + + 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 { ... }" + } + + if (!entity.shouldInlineExport) { + for (const exportName of entity.exportNames) { + DtsEmitHelpers.emitNamedExport(writer, exportName, entity); + } + } + + writer.ensureSkippedLine(); + } + + DtsEmitHelpers.emitStarExports(writer, collector); + + // Emit "export { }" which is a special directive that prevents consumers from importing declarations + // that don't have an explicit "export" modifier. + writer.ensureSkippedLine(); + writer.writeLine('export { }'); + } + + /** + * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. + */ + private static _modifySpan( + collector: Collector, + span: Span, + entity: CollectorEntity, + astDeclaration: AstDeclaration, + dtsKind: DtsRollupKind, + ): void { + const previousSpan: Span | undefined = span.previousSibling; + + let recurseChildren = true; + switch (span.kind) { + case ts.SyntaxKind.JSDocComment: + // If the @packageDocumentation comment seems to be attached to one of the regular API items, + // omit it. It gets explictly emitted at the top of the file. + if (/[\s*]@packagedocumentation[\s*]/gi.test(span.node.getText())) { + span.modification.skipAll(); + } + + // For now, we don't transform JSDoc comment nodes at all + recurseChildren = false; + break; + + case ts.SyntaxKind.ExportKeyword: + case ts.SyntaxKind.DefaultKeyword: + case ts.SyntaxKind.DeclareKeyword: + // Delete any explicit "export" or "declare" keywords -- we will re-add them below + span.modification.skipAll(); + break; + + case ts.SyntaxKind.InterfaceKeyword: + case ts.SyntaxKind.ClassKeyword: + case ts.SyntaxKind.EnumKeyword: + case ts.SyntaxKind.NamespaceKeyword: + case ts.SyntaxKind.ModuleKeyword: + case ts.SyntaxKind.TypeKeyword: + case ts.SyntaxKind.FunctionKeyword: + // Replace the stuff we possibly deleted above + let replacedModifiers = ''; + + // Add a declare statement for root declarations (but not for nested declarations) + if (!astDeclaration.parent) { + replacedModifiers += 'declare '; + } + + if (entity.shouldInlineExport) { + replacedModifiers = 'export ' + replacedModifiers; + } + + if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { + // If there is a previous span of type SyntaxList, then apply it before any other modifiers + // (e.g. "abstract") that appear there. + previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; + } else { + // Otherwise just stick it in front of this span + span.modification.prefix = replacedModifiers + span.modification.prefix; + } + + break; + + case ts.SyntaxKind.VariableDeclaration: + // Is this a top-level variable declaration? + // (The logic below does not apply to variable declarations that are part of an explicit "namespace" block, + // since the compiler prefers not to emit "declare" or "export" keywords for those declarations.) + if (!span.parent) { + // The VariableDeclaration node is part of a VariableDeclarationList, however + // the Entry.followedSymbol points to the VariableDeclaration part because + // multiple definitions might share the same VariableDeclarationList. + // + // Since we are emitting a separate declaration for each one, we need to look upwards + // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList + // content (e.g. "var" from "var x=1, y=2"). + const list: ts.VariableDeclarationList | undefined = TypeScriptHelpers.matchAncestor(span.node, [ + ts.SyntaxKind.VariableDeclarationList, + ts.SyntaxKind.VariableDeclaration, + ]); + if (!list) { + // This should not happen unless the compiler API changes somehow + throw new InternalError('Unsupported variable declaration'); + } + + const listPrefix: string = list.getSourceFile().text.slice(list.getStart(), list.declarations[0]!.getStart()); + span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; + span.modification.suffix = ';'; + + if (entity.shouldInlineExport) { + span.modification.prefix = 'export ' + span.modification.prefix; + } + + const declarationMetadata: DeclarationMetadata = collector.fetchDeclarationMetadata(astDeclaration); + if (declarationMetadata.tsdocParserContext) { + // Typically the comment for a variable declaration is attached to the outer variable statement + // (which may possibly contain multiple variable declarations), so it's not part of the Span. + // Instead we need to manually inject it. + let originalComment: string = declarationMetadata.tsdocParserContext.sourceRange.toString(); + if (!/\r?\n\s*$/.test(originalComment)) { + originalComment += '\n'; + } + + span.modification.indentDocComment = IndentDocCommentScope.PrefixOnly; + span.modification.prefix = originalComment + span.modification.prefix; + } + } + + break; + + case ts.SyntaxKind.Identifier: + { + const referencedEntity: CollectorEntity | undefined = collector.tryGetEntityForNode( + span.node as ts.Identifier, + ); + + if (referencedEntity) { + if (!referencedEntity.nameForEmit) { + // This should never happen + throw new InternalError('referencedEntry.nameForEmit is undefined'); + } + + span.modification.prefix = referencedEntity.nameForEmit; + // For debugging: + // span.modification.prefix += '/*R=FIX*/'; + } else { + // For debugging: + // span.modification.prefix += '/*R=KEEP*/'; + } + } + + break; + + case ts.SyntaxKind.ImportType: + DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => { + DtsRollupGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, dtsKind); + }); + break; + + default: + break; + } + + if (recurseChildren) { + for (const child of span.children) { + let childAstDeclaration: AstDeclaration = astDeclaration; + + // Should we trim this node? + let trimmed = false; + if (AstDeclaration.isSupportedSyntaxKind(child.kind)) { + childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); + const releaseTag: ReleaseTag = collector.fetchApiItemMetadata(childAstDeclaration).effectiveReleaseTag; + + if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { + let nodeToTrim: Span = child; + + // If we are trimming a variable statement, then we need to trim the outer VariableDeclarationList + // as well. + if (child.kind === ts.SyntaxKind.VariableDeclaration) { + const variableStatement: Span | undefined = child.findFirstParent(ts.SyntaxKind.VariableStatement); + if (variableStatement !== undefined) { + nodeToTrim = variableStatement; + } + } + + const modification: SpanModification = nodeToTrim.modification; + + // Yes, trim it and stop here + const name: string = childAstDeclaration.astSymbol.localName; + modification.omitChildren = true; + + if (collector.extractorConfig.omitTrimmingComments) { + modification.prefix = ''; + } else { + modification.prefix = `/* Excluded from this release type: ${name} */`; + } + + modification.suffix = ''; + + if (nodeToTrim.children.length > 0) { + // If there are grandchildren, then keep the last grandchild's separator, + // since it often has useful whitespace + modification.suffix = nodeToTrim.children[nodeToTrim.children.length - 1]!.separator; + } + + if ( + nodeToTrim.nextSibling && // If the thing we are trimming is followed by a comma, then trim the comma also. + // An example would be an enum member. + nodeToTrim.nextSibling.kind === ts.SyntaxKind.CommaToken + ) { + // Keep its separator since it often has useful whitespace + modification.suffix += nodeToTrim.nextSibling.separator; + nodeToTrim.nextSibling.modification.skipAll(); + } + + trimmed = true; + } + } + + if (!trimmed) { + DtsRollupGenerator._modifySpan(collector, child, entity, childAstDeclaration, dtsKind); + } + } + } + } + + private static _shouldIncludeReleaseTag(releaseTag: ReleaseTag, dtsKind: DtsRollupKind): boolean { + switch (dtsKind) { + case DtsRollupKind.InternalRelease: + return true; + case DtsRollupKind.AlphaRelease: + return ( + releaseTag === ReleaseTag.Alpha || + releaseTag === ReleaseTag.Beta || + releaseTag === ReleaseTag.Public || + // NOTE: If the release tag is "None", then we don't have enough information to trim it + releaseTag === ReleaseTag.None + ); + case DtsRollupKind.BetaRelease: + return ( + releaseTag === ReleaseTag.Beta || + releaseTag === ReleaseTag.Public || + // NOTE: If the release tag is "None", then we don't have enough information to trim it + releaseTag === ReleaseTag.None + ); + case DtsRollupKind.PublicRelease: + return releaseTag === ReleaseTag.Public || releaseTag === ReleaseTag.None; + default: + throw new Error(`${DtsRollupKind[dtsKind]} is not implemented`); + } + } +} diff --git a/packages/api-extractor/src/generators/ExcerptBuilder.ts b/packages/api-extractor/src/generators/ExcerptBuilder.ts new file mode 100644 index 000000000..24798e172 --- /dev/null +++ b/packages/api-extractor/src/generators/ExcerptBuilder.ts @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ExcerptTokenKind, type IExcerptToken, type IExcerptTokenRange } from '@discordjs/api-extractor-model'; +import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference'; +import * as ts from 'typescript'; +import type { AstDeclaration } from '../analyzer/AstDeclaration.js'; +import { Span } from '../analyzer/Span.js'; +import type { DeclarationReferenceGenerator } from './DeclarationReferenceGenerator.js'; + +/** + * Used to provide ExcerptBuilder with a list of nodes whose token range we want to capture. + */ +export interface IExcerptBuilderNodeToCapture { + /** + * The node to capture + */ + node: ts.Node | undefined; + /** + * The token range whose startIndex/endIndex will be overwritten with the indexes for the + * tokens corresponding to IExcerptBuilderNodeToCapture.node + */ + tokenRange: IExcerptTokenRange; +} + +/** + * Internal state for ExcerptBuilder + */ +interface IBuildSpanState { + /** + * Tracks whether the last appended token was a separator. If so, and we're in the middle of + * capturing a token range, then omit the separator from the range. + */ + lastAppendedTokenIsSeparator: boolean; + + referenceGenerator: DeclarationReferenceGenerator; + + /** + * The AST node that we will traverse to extract tokens + */ + startingNode: ts.Node; + + /** + * Normally, the excerpt will include all child nodes for `startingNode`; whereas if `childKindToStopBefore` + * is specified, then the node traversal will stop before (i.e. excluding) the first immediate child + * of `startingNode` with the specified syntax kind. + * + * @remarks + * For example, suppose the signature is `interface X: Y { z: string }`. The token `{` has syntax kind + * `ts.SyntaxKind.FirstPunctuation`, so we can specify that to truncate the excerpt to `interface X: Y`. + */ + stopBeforeChildKind: ts.SyntaxKind | undefined; + + tokenRangesByNode: Map; +} + +export class ExcerptBuilder { + /** + * Appends a blank line to the `excerptTokens` list. + * + * @param excerptTokens - The target token list to append to + */ + public static addBlankLine(excerptTokens: IExcerptToken[]): void { + let newlines = '\n\n'; + // If the existing text already ended with a newline, then only append one newline + if (excerptTokens.length > 0) { + const previousText: string = excerptTokens[excerptTokens.length - 1]!.text; + if (previousText.endsWith('\n')) { + newlines = '\n'; + } + } + + excerptTokens.push({ kind: ExcerptTokenKind.Content, text: newlines }); + } + + /** + * Appends the signature for the specified `AstDeclaration` to the `excerptTokens` list. + * + * @param excerptTokens - The target token list to append to + * @param astDeclaration - The declaration + * @param nodesToCapture - A list of child nodes whose token ranges we want to capture + */ + public static addDeclaration( + excerptTokens: IExcerptToken[], + astDeclaration: AstDeclaration, + nodesToCapture: IExcerptBuilderNodeToCapture[], + referenceGenerator: DeclarationReferenceGenerator, + ): void { + let stopBeforeChildKind: ts.SyntaxKind | undefined; + + switch (astDeclaration.declaration.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + // FirstPunctuation = "{" + stopBeforeChildKind = ts.SyntaxKind.FirstPunctuation; + break; + case ts.SyntaxKind.ModuleDeclaration: + // ModuleBlock = the "{ ... }" block + stopBeforeChildKind = ts.SyntaxKind.ModuleBlock; + break; + default: + break; + } + + const span: Span = new Span(astDeclaration.declaration); + + const tokenRangesByNode: Map = new Map(); + for (const excerpt of nodesToCapture || []) { + if (excerpt.node) { + tokenRangesByNode.set(excerpt.node, excerpt.tokenRange); + } + } + + ExcerptBuilder._buildSpan(excerptTokens, span, { + referenceGenerator, + startingNode: span.node, + stopBeforeChildKind, + tokenRangesByNode, + lastAppendedTokenIsSeparator: false, + }); + ExcerptBuilder._condenseTokens(excerptTokens, [...tokenRangesByNode.values()]); + } + + public static createEmptyTokenRange(): IExcerptTokenRange { + return { startIndex: 0, endIndex: 0 }; + } + + private static _buildSpan(excerptTokens: IExcerptToken[], span: Span, state: IBuildSpanState): boolean { + if (span.kind === ts.SyntaxKind.JSDocComment) { + // Discard any comments + return true; + } + + // Can this node start a excerpt? + const capturedTokenRange: IExcerptTokenRange | undefined = state.tokenRangesByNode.get(span.node); + let excerptStartIndex = 0; + + if (capturedTokenRange) { + // We will assign capturedTokenRange.startIndex to be the index of the next token to be appended + excerptStartIndex = excerptTokens.length; + } + + if (span.prefix) { + let canonicalReference: DeclarationReference | undefined; + + if (span.kind === ts.SyntaxKind.Identifier) { + const name: ts.Identifier = span.node as ts.Identifier; + if (!ExcerptBuilder._isDeclarationName(name)) { + canonicalReference = state.referenceGenerator.getDeclarationReferenceForIdentifier(name); + } + } + + if (canonicalReference) { + ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix, canonicalReference); + } else { + ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.prefix); + } + + state.lastAppendedTokenIsSeparator = false; + } + + for (const child of span.children) { + if (span.node === state.startingNode && state.stopBeforeChildKind && child.kind === state.stopBeforeChildKind) { + // We reached a child whose kind is stopBeforeChildKind, so stop traversing + return false; + } + + if (!this._buildSpan(excerptTokens, child, state)) { + return false; + } + } + + if (span.suffix) { + ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.suffix); + state.lastAppendedTokenIsSeparator = false; + } + + if (span.separator) { + ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.separator); + state.lastAppendedTokenIsSeparator = true; + } + + // Are we building a excerpt? If so, set its range + if (capturedTokenRange) { + capturedTokenRange.startIndex = excerptStartIndex; + + // We will assign capturedTokenRange.startIndex to be the index after the last token + // that was appended so far. However, if the last appended token was a separator, omit + // it from the range. + let excerptEndIndex: number = excerptTokens.length; + if (state.lastAppendedTokenIsSeparator) { + excerptEndIndex--; + } + + capturedTokenRange.endIndex = excerptEndIndex; + } + + return true; + } + + private static _appendToken( + excerptTokens: IExcerptToken[], + excerptTokenKind: ExcerptTokenKind, + text: string, + canonicalReference?: DeclarationReference, + ): void { + if (text.length === 0) { + return; + } + + const excerptToken: IExcerptToken = { kind: excerptTokenKind, text }; + if (canonicalReference !== undefined) { + excerptToken.canonicalReference = canonicalReference.toString(); + } + + excerptTokens.push(excerptToken); + } + + /** + * Condenses the provided excerpt tokens by merging tokens where possible. Updates the provided token ranges to + * remain accurate after token merging. + * + * @remarks + * For example, suppose we have excerpt tokens ["A", "B", "C"] and a token range [0, 2]. If the excerpt tokens + * are condensed to ["AB", "C"], then the token range would be updated to [0, 1]. Note that merges are only + * performed if they are compatible with the provided token ranges. In the example above, if our token range was + * originally [0, 1], we would not be able to merge tokens "A" and "B". + */ + private static _condenseTokens(excerptTokens: IExcerptToken[], tokenRanges: IExcerptTokenRange[]): void { + // This set is used to quickly lookup a start or end index. + const startOrEndIndices: Set = new Set(); + for (const tokenRange of tokenRanges) { + startOrEndIndices.add(tokenRange.startIndex); + startOrEndIndices.add(tokenRange.endIndex); + } + + for (let currentIndex = 1; currentIndex < excerptTokens.length; ++currentIndex) { + while (currentIndex < excerptTokens.length) { + const prevPrevToken: IExcerptToken | undefined = excerptTokens[currentIndex - 2]; // May be undefined + const prevToken: IExcerptToken = excerptTokens[currentIndex - 1]!; + const currentToken: IExcerptToken = excerptTokens[currentIndex]!; + + // The number of excerpt tokens that are merged in this iteration. We need this to determine + // how to update the start and end indices of our token ranges. + let mergeCount: number; + + // There are two types of merges that can occur. We only perform these merges if they are + // compatible with all of our token ranges. + if ( + prevPrevToken && + prevPrevToken.kind === ExcerptTokenKind.Reference && + prevToken.kind === ExcerptTokenKind.Content && + prevToken.text.trim() === '.' && + currentToken.kind === ExcerptTokenKind.Reference && + !startOrEndIndices.has(currentIndex) && + !startOrEndIndices.has(currentIndex - 1) + ) { + // If the current token is a reference token, the previous token is a ".", and the previous- + // previous token is a reference token, then merge all three tokens into a reference token. + // + // For example: Given ["MyNamespace" (R), ".", "MyClass" (R)], tokens "." and "MyClass" might + // be merged into "MyNamespace". The condensed token would be ["MyNamespace.MyClass" (R)]. + prevPrevToken.text += prevToken.text + currentToken.text; + prevPrevToken.canonicalReference = currentToken.canonicalReference; + mergeCount = 2; + currentIndex--; + } else if ( + // If the current and previous tokens are both content tokens, then merge the tokens into a + // single content token. For example: Given ["export ", "declare class"], these tokens + // might be merged into "export declare class". + prevToken.kind === ExcerptTokenKind.Content && + prevToken.kind === currentToken.kind && + !startOrEndIndices.has(currentIndex) + ) { + prevToken.text += currentToken.text; + mergeCount = 1; + } else { + // Otherwise, no merging can occur here. Continue to the next index. + break; + } + + // Remove the now redundant excerpt token(s), as they were merged into a previous token. + excerptTokens.splice(currentIndex, mergeCount); + + // Update the start and end indices for all token ranges based upon how many excerpt + // tokens were merged and in what positions. + for (const tokenRange of tokenRanges) { + if (tokenRange.startIndex > currentIndex) { + tokenRange.startIndex -= mergeCount; + } + + if (tokenRange.endIndex > currentIndex) { + tokenRange.endIndex -= mergeCount; + } + } + + // Clear and repopulate our set with the updated indices. + startOrEndIndices.clear(); + for (const tokenRange of tokenRanges) { + startOrEndIndices.add(tokenRange.startIndex); + startOrEndIndices.add(tokenRange.endIndex); + } + } + } + } + + private static _isDeclarationName(name: ts.Identifier): boolean { + return ExcerptBuilder._isDeclaration(name.parent) && name.parent.name === name; + } + + private static _isDeclaration(node: ts.Node): node is ts.NamedDeclaration { + switch (node.kind) { + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.VariableDeclaration: + case ts.SyntaxKind.Parameter: + case ts.SyntaxKind.EnumDeclaration: + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: + case ts.SyntaxKind.ModuleDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.MethodSignature: + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.PropertySignature: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.InterfaceDeclaration: + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.TypeParameter: + case ts.SyntaxKind.EnumMember: + case ts.SyntaxKind.BindingElement: + return true; + default: + return false; + } + } +} diff --git a/packages/api-extractor/src/generators/IndentedWriter.ts b/packages/api-extractor/src/generators/IndentedWriter.ts new file mode 100644 index 000000000..b6c08cd0b --- /dev/null +++ b/packages/api-extractor/src/generators/IndentedWriter.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { StringBuilder, type IStringBuilder } from '@rushstack/node-core-library'; + +/** + * A utility for writing indented text. + * + * @remarks + * + * Note that the indentation is inserted at the last possible opportunity. + * For example, this code... + * + * ```ts + * writer.write('begin\n'); + * writer.increaseIndent(); + * writer.write('one\ntwo\n'); + * writer.decreaseIndent(); + * writer.increaseIndent(); + * writer.decreaseIndent(); + * writer.write('end'); + * ``` + * + * ...would produce this output: + * + * ``` + * begin + * one + * two + * end + * ``` + */ +export class IndentedWriter { + /** + * The text characters used to create one level of indentation. + * Two spaces by default. + */ + public defaultIndentPrefix: string = ' '; + + /** + * Whether to indent blank lines + */ + public indentBlankLines: boolean = false; + + /** + * Trims leading spaces from the input text before applying the indent. + * + * @remarks + * Consider the following example: + * + * ```ts + * indentedWriter.increaseIndent(' '); // four spaces + * indentedWriter.write(' a\n b c\n'); + * indentedWriter.decreaseIndent(); + * ``` + * + * Normally the output would be indented by 6 spaces: 4 from `increaseIndent()`, plus the 2 spaces + * from `write()`: + * ``` + * a + * b c + * ``` + * + * Setting `trimLeadingSpaces=true` will trim the leading spaces, so that the lines are indented + * by 4 spaces only: + * ``` + * a + * b c + * ``` + */ + public trimLeadingSpaces: boolean = false; + + private readonly _builder: IStringBuilder; + + private _latestChunk: string | undefined; + + private _previousChunk: string | undefined; + + private _atStartOfLine: boolean; + + private readonly _indentStack: string[]; + + private _indentText: string; + + private _previousLineIsBlank: boolean; + + private _currentLineIsBlank: boolean; + + public constructor(builder?: IStringBuilder) { + this._builder = builder ?? new StringBuilder(); + this._latestChunk = undefined; + this._previousChunk = undefined; + this._atStartOfLine = true; + this._previousLineIsBlank = true; + this._currentLineIsBlank = true; + + this._indentStack = []; + this._indentText = ''; + } + + /** + * Retrieves the output that was built so far. + */ + public getText(): string { + return this._builder.toString(); + } + + public toString(): string { + return this.getText(); + } + + /** + * Increases the indentation. Normally the indentation is two spaces, + * however an arbitrary prefix can optional be specified. (For example, + * the prefix could be "// " to indent and comment simultaneously.) + * Each call to IndentedWriter.increaseIndent() must be followed by a + * corresponding call to IndentedWriter.decreaseIndent(). + */ + public increaseIndent(indentPrefix?: string): void { + this._indentStack.push(indentPrefix ?? this.defaultIndentPrefix); + this._updateIndentText(); + } + + /** + * Decreases the indentation, reverting the effect of the corresponding call + * to IndentedWriter.increaseIndent(). + */ + public decreaseIndent(): void { + this._indentStack.pop(); + this._updateIndentText(); + } + + /** + * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur + * in pairs. + */ + public indentScope(scope: () => void, indentPrefix?: string): void { + this.increaseIndent(indentPrefix); + scope(); + this.decreaseIndent(); + } + + /** + * Adds a newline if the file pointer is not already at the start of the line (or start of the stream). + */ + public ensureNewLine(): void { + const lastCharacter: string = this.peekLastCharacter(); + if (lastCharacter !== '\n' && lastCharacter !== '') { + this._writeNewLine(); + } + } + + /** + * Adds up to two newlines to ensure that there is a blank line above the current position. + * The start of the stream is considered to be a blank line, so `ensureSkippedLine()` has no effect + * unless some text has been written. + */ + public ensureSkippedLine(): void { + this.ensureNewLine(); + if (!this._previousLineIsBlank) { + this._writeNewLine(); + } + } + + /** + * Returns the last character that was written, or an empty string if no characters have been written yet. + */ + public peekLastCharacter(): string { + if (this._latestChunk !== undefined) { + return this._latestChunk.slice(-1, -1 + 1); + } + + return ''; + } + + /** + * Returns the second to last character that was written, or an empty string if less than one characters + * have been written yet. + */ + public peekSecondLastCharacter(): string { + if (this._latestChunk !== undefined) { + if (this._latestChunk.length > 1) { + return this._latestChunk.slice(-2, -2 + 1); + } + + if (this._previousChunk !== undefined) { + return this._previousChunk.slice(-1, -1 + 1); + } + } + + return ''; + } + + /** + * Writes some text to the internal string buffer, applying indentation according + * to the current indentation level. If the string contains multiple newlines, + * each line will be indented separately. + */ + public write(message: string): void { + if (message.length === 0) { + return; + } + + // If there are no newline characters, then append the string verbatim + if (!/[\n\r]/.test(message)) { + this._writeLinePart(message); + return; + } + + // Otherwise split the lines and write each one individually + let first = true; + for (const linePart of message.split('\n')) { + if (first) { + first = false; + } else { + this._writeNewLine(); + } + + if (linePart) { + this._writeLinePart(linePart.replaceAll('\r', '')); + } + } + } + + /** + * A shorthand for writing an optional message, followed by a newline. + * Indentation is applied following the semantics of IndentedWriter.write(). + */ + public writeLine(message: string = ''): void { + if (message.length > 0) { + this.write(message); + } + + this._writeNewLine(); + } + + /** + * Writes a string that does not contain any newline characters. + */ + private _writeLinePart(message: string): void { + let trimmedMessage: string = message; + + if (this.trimLeadingSpaces && this._atStartOfLine) { + trimmedMessage = message.replace(/^ +/, ''); + } + + if (trimmedMessage.length > 0) { + if (this._atStartOfLine && this._indentText.length > 0) { + this._write(this._indentText); + } + + this._write(trimmedMessage); + if (this._currentLineIsBlank && /\S/.test(trimmedMessage)) { + this._currentLineIsBlank = false; + } + + this._atStartOfLine = false; + } + } + + private _writeNewLine(): void { + if (this.indentBlankLines && this._atStartOfLine && this._indentText.length > 0) { + this._write(this._indentText); + } + + this._previousLineIsBlank = this._currentLineIsBlank; + this._write('\n'); + this._currentLineIsBlank = true; + this._atStartOfLine = true; + } + + private _write(str: string): void { + this._previousChunk = this._latestChunk; + this._latestChunk = str; + this._builder.append(str); + } + + private _updateIndentText(): void { + this._indentText = this._indentStack.join(''); + } +} diff --git a/packages/api-extractor/src/generators/test/IndentedWriter.test.ts b/packages/api-extractor/src/generators/test/IndentedWriter.test.ts new file mode 100644 index 000000000..f875c2665 --- /dev/null +++ b/packages/api-extractor/src/generators/test/IndentedWriter.test.ts @@ -0,0 +1,119 @@ +// 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(); +}); diff --git a/packages/api-extractor/src/generators/test/__snapshots__/IndentedWriter.test.ts.snap b/packages/api-extractor/src/generators/test/__snapshots__/IndentedWriter.test.ts.snap new file mode 100644 index 000000000..6803fe9f2 --- /dev/null +++ b/packages/api-extractor/src/generators/test/__snapshots__/IndentedWriter.test.ts.snap @@ -0,0 +1,65 @@ +// 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 +" +`; diff --git a/packages/api-extractor/src/index.ts b/packages/api-extractor/src/index.ts new file mode 100644 index 000000000..14a61e457 --- /dev/null +++ b/packages/api-extractor/src/index.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * API Extractor helps with validation, documentation, and reviewing of the exported API for a TypeScript library. + * The `@microsoft/api-extractor` package provides the command-line tool. It also exposes a developer API that you + * can use to invoke API Extractor programmatically. + * + * @packageDocumentation + */ + +export { ConsoleMessageId } from './api/ConsoleMessageId.js'; + +export { CompilerState, type ICompilerStateCreateOptions } from './api/CompilerState.js'; + +export { Extractor, type IExtractorInvokeOptions, ExtractorResult } from './api/Extractor.js'; + +export { + type IExtractorConfigPrepareOptions, + type IExtractorConfigLoadForFolderOptions, + ExtractorConfig, +} from './api/ExtractorConfig.js'; + +export { ExtractorLogLevel } from './api/ExtractorLogLevel.js'; + +export { + ExtractorMessage, + type IExtractorMessageProperties, + ExtractorMessageCategory, +} from './api/ExtractorMessage.js'; + +export { ExtractorMessageId } from './api/ExtractorMessageId.js'; + +export type { + IConfigCompiler, + IConfigApiReport, + IConfigDocModel, + IConfigDtsRollup, + IConfigTsdocMetadata, + IConfigMessageReportingRule, + IConfigMessageReportingTable, + IExtractorMessagesConfig, + IConfigFile, +} from './api/IConfigFile.js'; diff --git a/packages/api-extractor/src/schemas/api-extractor-defaults.json b/packages/api-extractor/src/schemas/api-extractor-defaults.json new file mode 100644 index 000000000..5aaf8b9ce --- /dev/null +++ b/packages/api-extractor/src/schemas/api-extractor-defaults.json @@ -0,0 +1,95 @@ +{ + "projectFolder": "", + + // ("mainEntryPointFilePath" is required) + + "bundledPackages": [], + + "newlineKind": "crlf", + + "enumMemberOrder": "by-name", + + "compiler": { + "tsconfigFilePath": "/tsconfig.json", + "skipLibCheck": false + }, + + "apiReport": { + // ("enabled" is required) + "reportFileName": ".api.md", + "reportFolder": "/etc/", + "reportTempFolder": "/temp/", + "includeForgottenExports": false + }, + + "docModel": { + // ("enabled" is required) + "apiJsonFilePath": "/temp/.api.json", + "includeForgottenExports": false + }, + + "dtsRollup": { + // ("enabled" is required) + + "untrimmedFilePath": "/dist/.d.ts", + "alphaTrimmedFilePath": "", + "betaTrimmedFilePath": "", + "publicTrimmedFilePath": "", + "omitTrimmingComments": false + }, + + "tsdocMetadata": { + "enabled": true, + "tsdocMetadataFilePath": "" + }, + + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + "ae-forgotten-export": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-incompatible-release-tags": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-internal-missing-underscore": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-internal-mixed-release-tag": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-undocumented": { + "logLevel": "none" + }, + "ae-unresolved-inheritdoc-reference": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-unresolved-inheritdoc-base": { + "logLevel": "warning", + "addToApiReportFile": true + }, + "ae-wrong-input-file-type": { + "logLevel": "error" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + } + } + }, + + "testMode": false +} diff --git a/packages/api-extractor/src/schemas/api-extractor-template.json b/packages/api-extractor/src/schemas/api-extractor-template.json new file mode 100644 index 000000000..efc6ae143 --- /dev/null +++ b/packages/api-extractor/src/schemas/api-extractor-template.json @@ -0,0 +1,427 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * 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 "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/lib/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Set to true when invoking API Extractor's test harness. When `testMode` is true, the `toolVersion` field in the + * .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests. + * + * DEFAULT VALUE: "false" + */ + // "testMode": false, + + /** + * Specifies how API Extractor sorts members of an enum when generating the .api.json file. By default, the output + * files will be sorted alphabetically, which is "by-name". To keep the ordering in the source code, specify + * "preserve". + * + * DEFAULT VALUE: "by-name" + */ + // "enumMemberOrder": "by-name", + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * 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 "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * 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 "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportFolder": "/temp/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/", + + /** + * 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. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json", + + /** + * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations + * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to + * learn more. + * + * DEFAULT VALUE: "false" + */ + // "includeForgottenExports": false, + + /** + * 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 `` 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. + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "projectFolderUrl": "http://github.com/path/to/your/projectFolder" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * 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". + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "alphaTrimmedFilePath": "/dist/-alpha.d.ts", + + /** + * 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". + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * 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 "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should 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 "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/packages/api-extractor/src/schemas/api-extractor.schema.json b/packages/api-extractor/src/schemas/api-extractor.schema.json new file mode 100644 index 000000000..507522730 --- /dev/null +++ b/packages/api-extractor/src/schemas/api-extractor.schema.json @@ -0,0 +1,229 @@ +{ + "title": "API Extractor Configuration", + "description": "Describes how the API Extractor tool will process a 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 another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects.", + "type": "string" + }, + + "projectFolder": { + "description": "Determines the \"\" token that can be used with other config file settings. The project folder typically contains the tsconfig.json and package.json config files, but the path is user-defined. The path is resolved relative to the folder of the config file that contains the setting. The default value for \"projectFolder\" is the token \"\", which means the folder is determined using the following heuristics:\n\nIf the config/rig.json system is used (as defined by @rushstack/rig-package), then the \"\" value will be the package folder that referenced the rig.\n\nOtherwise, the \"\" value is determined by traversing parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error will be reported.", + "type": "string" + }, + + "mainEntryPointFilePath": { + "description": "Specifies the .d.ts file to be used as the starting point for analysis. API Extractor analyzes the symbols exported by this module. The file extension must be \".d.ts\" and not \".ts\". 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 \"\".", + "type": "string" + }, + + "bundledPackages": { + "description": "A list of NPM package names whose exports should be treated as part of this package.", + "type": "array", + "items": { + "type": "string" + } + }, + + "enumMemberOrder": { + "description": "Specifies how API Extractor sorts the members of an enum when generating the .api.json doc model. \n 'by-name': sort the items according to the enum member name \n 'preserve': keep the original order that items appear in the source code", + "type": "string", + "enum": ["by-name", "preserve"], + "default": "by-name" + }, + + "compiler": { + "description": "Determines how the TypeScript compiler engine will be invoked by API Extractor.", + "type": "object", + "properties": { + "tsconfigFilePath": { + "description": "Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. 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 \"\". Note: This setting will be ignored if \"overrideTsconfig\" is used.", + "type": "string" + }, + "overrideTsconfig": { + "description": "Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. The object must conform to the TypeScript tsconfig schema: http://json.schemastore.org/tsconfig If omitted, then the tsconfig.json file will be read from the \"projectFolder\".", + "type": "object" + }, + "skipLibCheck": { + "description": "This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.", + "type": "boolean" + } + }, + "additionalProperties": false + }, + + "apiReport": { + "description": "Configures how the API report file (*.api.md) will be generated.", + "type": "object", + "properties": { + "enabled": { + "description": "Whether to generate an API report.", + "type": "boolean" + }, + + "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" + }, + + "reportFolder": { + "description": "Specifies the folder where the API report file is written. The file name portion is determined by the \"reportFileName\" setting. The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, e.g. for an API review. 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 \"\".", + "type": "string" + }, + + "reportTempFolder": { + "description": "Specifies the folder where the temporary report file is written. The file name portion is determined by the \"reportFileName\" setting. After the temporary file is written to disk, it is compared with the file in the \"reportFolder\". If they are different, a production build will fail. 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 \"\".", + "type": "string" + }, + + "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" + } + }, + "required": ["enabled"], + "additionalProperties": false + }, + + "docModel": { + "description": "Configures how the doc model file (*.api.json) will be generated.", + "type": "object", + "properties": { + "enabled": { + "description": "Whether to generate doc model file.", + "type": "boolean" + }, + "apiJsonFilePath": { + "description": "The output path for the doc model file. The file extension should be \".api.json\". 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 \"\".", + "type": "string" + }, + "includeForgottenExports": { + "description": "Whether \"forgotten exports\" should be included in the doc model 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" + }, + "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 `` 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" + } + }, + "required": ["enabled"], + "additionalProperties": false + }, + + "dtsRollup": { + "description": "Configures how the .d.ts rollup file will be generated.", + "type": "object", + "properties": { + "enabled": { + "description": "Whether to generate the .d.ts rollup file.", + "type": "boolean" + }, + "untrimmedFilePath": { + "description": "Specifies the output path for a .d.ts rollup file to be generated without any trimming. This file will include all declarations that are exported by the main entry point. 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 \"\".", + "type": "string" + }, + "alphaTrimmedFilePath": { + "description": "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\". 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 \"\".", + "type": "string" + }, + "betaTrimmedFilePath": { + "description": "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\". 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 \"\".", + "type": "string" + }, + "publicTrimmedFilePath": { + "description": "Specifies the output path for a .d.ts rollup file to be generated with trimming for a \"public\" release. This file will include only declarations that are marked as \"@public\". 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 \"\".", + "type": "string" + }, + "omitTrimmingComments": { + "description": "When a declaration is trimmed, by default it will be replaced by a code comment such as \"Excluded from this release type: exampleMember\". Set \"omitTrimmingComments\" to true to remove the declaration completely.", + "type": "boolean" + } + }, + "required": ["enabled"], + "additionalProperties": false + }, + + "tsdocMetadata": { + "description": "Configures how the tsdoc-metadata.json file will be generated.", + "type": "object", + "properties": { + "enabled": { + "description": "Whether to generate the tsdoc-metadata.json file.", + "type": "boolean" + }, + "tsdocMetadataFilePath": { + "description": "Specifies where the TSDoc metadata file should 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 \"\". The default value is \"\", which causes the path to be automatically inferred from the \"tsdocMetadata\", \"typings\" or \"main\" fields of the project's package.json. If none of these fields are set, the lookup falls back to \"tsdoc-metadata.json\" in the package folder.", + "type": "string" + } + }, + "additionalProperties": false + }, + + "newlineKind": { + "description": "Specifies what type of newlines API Extractor should use when writing output files. By default, the output files will be written with Windows-style newlines. To use POSIX-style newlines, specify \"lf\" instead. To use the OS's default newline kind, specify \"os\".", + "type": "string", + "enum": ["crlf", "lf", "os"], + "default": "crlf" + }, + + "messages": { + "description": "Configures how API Extractor reports error and warning messages produced during analysis.", + "type": "object", + "properties": { + "compilerMessageReporting": { + "description": "Configures handling of diagnostic messages generating the TypeScript compiler while analyzing the input .d.ts files.", + "$ref": "#/definitions/extractorMessageReportingTable" + }, + "extractorMessageReporting": { + "description": "Configures handling of messages reported by API Extractor during its analysis.", + "$ref": "#/definitions/extractorMessageReportingTable" + }, + "tsdocMessageReporting": { + "description": "Configures handling of messages reported by the TSDoc parser when analyzing code comments.", + "$ref": "#/definitions/extractorMessageReportingTable" + } + }, + "additionalProperties": false + }, + + "testMode": { + "description": "Set to true invoking API Extractor's test harness. When \"testMode\" is true, the \"toolVersion\" field in the .api.json file is assigned an empty string to prevent spurious diffs in output files tracked for tests.", + "type": "boolean" + } + }, + "required": ["mainEntryPointFilePath"], + "additionalProperties": false, + + "definitions": { + "extractorMessageReportingTable": { + "type": "object", + "description": "Specifies a table of reporting rules for different message identifiers, and also the default rule used for identifiers that do not appear in the table. The key is a message identifier for the associated type of message, or \"default\" to specify the default policy. For example, the key might be \"TS2551\" (a compiler message), \"tsdoc-link-tag-unescaped-text\" (a TSDOc message), or \"ae-extra-release-tag\" (a message related to the API Extractor analysis).", + "patternProperties": { + ".+": { + "type": "object", + "description": "Configures reporting for a given message identifier.", + "properties": { + "logLevel": { + "type": "string", + "description": "Specifies whether the message should be written to the the tool's output log. Note that the \"addToApiReportFile\" property may supersede this option.", + "enum": ["error", "warning", "none"] + }, + "addToApiReportFile": { + "type": "boolean", + "description": "If API Extractor is configured to write an API review file (.api.md), then the message will be written inside that file. If the API review file is NOT being written, then the message is instead logged according to the \"logLevel\" option." + } + }, + "additionalProperties": false, + "required": ["logLevel"] + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/api-extractor/src/start.ts b/packages/api-extractor/src/start.ts new file mode 100644 index 000000000..87fa9ebe3 --- /dev/null +++ b/packages/api-extractor/src/start.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as os from 'node:os'; +import * as process from 'node:process'; +import colors from 'colors'; +import { Extractor } from './api/Extractor.js'; +import { ApiExtractorCommandLine } from './cli/ApiExtractorCommandLine.js'; + +console.log( + os.EOL + colors.bold(`api-extractor ${Extractor.version} ` + colors.cyan(' - https://api-extractor.com/') + os.EOL), +); + +const parser: ApiExtractorCommandLine = new ApiExtractorCommandLine(); + +// eslint-disable-next-line promise/prefer-await-to-callbacks +parser.execute().catch((error) => { + console.error(colors.red(`An unexpected error occurred:`), error); + process.exit(1); +}); diff --git a/packages/api-extractor/tsconfig.eslint.json b/packages/api-extractor/tsconfig.eslint.json new file mode 100644 index 000000000..0aa3b9660 --- /dev/null +++ b/packages/api-extractor/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true + }, + "include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src"] +} diff --git a/packages/api-extractor/tsconfig.json b/packages/api-extractor/tsconfig.json new file mode 100644 index 000000000..0fd5e601b --- /dev/null +++ b/packages/api-extractor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + + "compilerOptions": { + "types": ["jest", "node"], + "isolatedModules": false, + "outDir": "./dist", + "esModuleInterop": true + } +} diff --git a/packages/api-extractor/tsup.config.ts b/packages/api-extractor/tsup.config.ts new file mode 100644 index 000000000..6db52b18f --- /dev/null +++ b/packages/api-extractor/tsup.config.ts @@ -0,0 +1,7 @@ +import { createTsupConfig } from '../../tsup.config.js'; + +export default createTsupConfig({ + entry: ['src/**/*.ts'], + cjsInterop: true, + noExternal: ['@microsoft/tsdoc*'], +}); diff --git a/packages/brokers/api-extractor-docs.json b/packages/brokers/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/brokers/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/brokers/package.json b/packages/brokers/package.json index 02fe90aea..b58ebe8a2 100644 --- a/packages/brokers/package.json +++ b/packages/brokers/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/brokers/*'", "release": "cliff-jumper" @@ -72,7 +72,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/builders/api-extractor-docs.json b/packages/builders/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/builders/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/builders/package.json b/packages/builders/package.json index cc0a39294..672b0b6e3 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/builders/*'", "release": "cliff-jumper" @@ -74,7 +74,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/collection/api-extractor-docs.json b/packages/collection/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/collection/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/collection/package.json b/packages/collection/package.json index fc7e71734..788855221 100644 --- a/packages/collection/package.json +++ b/packages/collection/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/collection/*'", "release": "cliff-jumper" @@ -61,7 +61,7 @@ "homepage": "https://discord.js.org", "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/core/api-extractor-docs.json b/packages/core/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/core/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/core/package.json b/packages/core/package.json index 126a963de..b3da21144 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -9,7 +9,7 @@ "build:docs": "tsc -p tsconfig.docs.json", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run build && pnpm run lint", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/core/*'", "release": "cliff-jumper" @@ -73,7 +73,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "18.18.8", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/create-discord-bot/package.json b/packages/create-discord-bot/package.json index e83c19c82..0096dea1a 100644 --- a/packages/create-discord-bot/package.json +++ b/packages/create-discord-bot/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@types/prompts": "^2.4.7", "@types/validate-npm-package-name": "^4.0.1", diff --git a/packages/formatters/api-extractor-docs.json b/packages/formatters/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/formatters/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/formatters/package.json b/packages/formatters/package.json index adb454e23..204941131 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -9,7 +9,7 @@ "build:docs": "tsc -p tsconfig.docs.json", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run build && pnpm run lint", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/formatters/*'", "release": "cliff-jumper" @@ -58,7 +58,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/next/api-extractor-docs.json b/packages/next/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/next/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/next/package.json b/packages/next/package.json index 1f6ae5118..31b736ca2 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -9,7 +9,7 @@ "build:docs": "tsc -p tsconfig.docs.json", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run build && pnpm run lint", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/next/*'", "release": "cliff-jumper" @@ -75,7 +75,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "18.18.8", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/proxy/api-extractor-docs.json b/packages/proxy/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/proxy/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/proxy/package.json b/packages/proxy/package.json index a80fe8ad1..96cfdbebc 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/proxy/*'", "release": "cliff-jumper" @@ -71,7 +71,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "18.18.8", "@types/supertest": "^2.0.15", "@vitest/coverage-v8": "^0.34.6", diff --git a/packages/rest/api-extractor-docs.json b/packages/rest/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/rest/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/rest/package.json b/packages/rest/package.json index bcbd5f58b..85dc0d7f0 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/rest/*'", "release": "cliff-jumper" @@ -94,7 +94,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "18.17.9", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 97204a11b..92a7c6fac 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -54,7 +54,7 @@ "homepage": "https://discord.js.org", "dependencies": { "@discordjs/api-extractor-utils": "workspace:^", - "@microsoft/api-extractor-model": "7.28.2", + "@discordjs/api-extractor-model": "workspace:^", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "0.16.2", "tslib": "^2.6.2", diff --git a/packages/scripts/src/generateIndex.ts b/packages/scripts/src/generateIndex.ts index eb70eddd5..9ae3541e2 100644 --- a/packages/scripts/src/generateIndex.ts +++ b/packages/scripts/src/generateIndex.ts @@ -1,7 +1,6 @@ import { stat, mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { cwd } from 'node:process'; -import { generatePath } from '@discordjs/api-extractor-utils'; import { ApiModel, ApiDeclaredItem, @@ -9,7 +8,8 @@ import { ApiItem, type ApiPackage, ApiItemKind, -} from '@microsoft/api-extractor-model'; +} from '@discordjs/api-extractor-model'; +import { generatePath } from '@discordjs/api-extractor-utils'; import { DocNodeKind, type DocCodeSpan, diff --git a/packages/scripts/turbo/generators/templates/default/api-extractor-docs.json b/packages/scripts/turbo/generators/templates/default/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/scripts/turbo/generators/templates/default/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/scripts/turbo/generators/templates/package.json.hbs b/packages/scripts/turbo/generators/templates/package.json.hbs index 077af2ca1..aa022e426 100644 --- a/packages/scripts/turbo/generators/templates/package.json.hbs +++ b/packages/scripts/turbo/generators/templates/package.json.hbs @@ -8,7 +8,7 @@ "build:docs": "tsc -p tsconfig.docs.json", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run build && pnpm run lint", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/{{name}}/*'", "release": "cliff-jumper" @@ -53,7 +53,7 @@ "dependencies": {}, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/util/api-extractor-docs.json b/packages/util/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/util/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/util/package.json b/packages/util/package.json index a77d10483..e591a45a2 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/util/*'", "release": "cliff-jumper" @@ -62,7 +62,7 @@ "homepage": "https://discord.js.org", "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "16.18.60", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/packages/voice/api-extractor-docs.json b/packages/voice/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/voice/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/voice/package.json b/packages/voice/package.json index 51e19bebb..7149f157b 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -10,7 +10,7 @@ "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run lint && pnpm run test && pnpm run build", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/voice/*'", "release": "cliff-jumper" @@ -73,7 +73,7 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.23.2", "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/jest": "^29.5.7", "@types/node": "16.18.60", "cross-env": "^7.0.3", diff --git a/packages/ws/api-extractor-docs.json b/packages/ws/api-extractor-docs.json deleted file mode 100644 index 225607dd2..000000000 --- a/packages/ws/api-extractor-docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./api-extractor.json", - "mainEntryPointFilePath": "/dist-docs/index.d.ts" -} diff --git a/packages/ws/package.json b/packages/ws/package.json index 3e039dce7..b950856eb 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -9,7 +9,7 @@ "build:docs": "tsc -p tsconfig.docs.json", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", - "docs": "pnpm run build:docs && api-extractor run --local && api-extractor run --local --config ./api-extractor-docs.json", + "docs": "pnpm run build:docs && api-extractor run --local", "prepack": "pnpm run build && pnpm run lint", "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/ws/*'", "release": "cliff-jumper" @@ -84,7 +84,7 @@ }, "devDependencies": { "@favware/cliff-jumper": "^2.2.1", - "@microsoft/api-extractor": "^7.38.2", + "@discordjs/api-extractor": "workspace:^", "@types/node": "18.17.9", "@vitest/coverage-v8": "^0.34.6", "cross-env": "^7.0.3", diff --git a/patches/eslint-plugin-i@2.29.0.patch b/patches/eslint-plugin-i@2.29.0.patch new file mode 100644 index 000000000..9876526c5 --- /dev/null +++ b/patches/eslint-plugin-i@2.29.0.patch @@ -0,0 +1,106 @@ +diff --git a/lib/rules/extensions.js b/lib/rules/extensions.js +index 3bd6529d40ae428e6efbda0fd6d48de10e7ea110..1e816a27d0d6db4281ffaab275343c1935bc29c1 100644 +--- a/lib/rules/extensions.js ++++ b/lib/rules/extensions.js +@@ -3,8 +3,8 @@ + var _resolve = require('eslint-module-utils/resolve');var _resolve2 = _interopRequireDefault(_resolve); + var _importType = require('../core/importType'); + var _moduleVisitor = require('eslint-module-utils/moduleVisitor');var _moduleVisitor2 = _interopRequireDefault(_moduleVisitor); +-var _docsUrl = require('../docsUrl');var _docsUrl2 = _interopRequireDefault(_docsUrl);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { 'default': obj };} +- ++var _docsUrl = require('../docsUrl');var _docsUrl2 = _interopRequireDefault(_docsUrl); ++var _has = require('has');var _has2 = _interopRequireDefault(_has);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { 'default': obj };} + var enumValues = { 'enum': ['always', 'ignorePackages', 'never'] }; + var patternProperties = { + type: 'object', +@@ -14,6 +14,7 @@ var properties = { + type: 'object', + properties: { + pattern: patternProperties, ++ checkTypeImports: { type: 'boolean' }, + ignorePackages: { type: 'boolean' } } }; + + +@@ -35,7 +36,7 @@ function buildProperties(context) { + } + + // If this is not the new structure, transfer all props to result.pattern +- if (obj.pattern === undefined && obj.ignorePackages === undefined) { ++ if (obj.pattern === undefined && obj.ignorePackages === undefined && obj.checkTypeImports === undefined) { + Object.assign(result.pattern, obj); + return; + } +@@ -49,6 +50,10 @@ function buildProperties(context) { + if (obj.ignorePackages !== undefined) { + result.ignorePackages = obj.ignorePackages; + } ++ ++ if (obj.checkTypeImports !== undefined) { ++ result.checkTypeImports = obj.checkTypeImports; ++ } + }); + + if (result.defaultConfig === 'ignorePackages') { +@@ -157,7 +162,7 @@ module.exports = { + + // get extension from resolved path, if possible. + // for unresolved, use source value. +- var extension = _path2['default'].extname(resolvedPath || importPath).substring(1); ++ var extension = _path2['default'].extname(resolvedPath || importPath).slice(1); + + // determine if this is a module + var isPackage = (0, _importType.isExternalModule)( +@@ -166,9 +171,10 @@ module.exports = { + context) || + (0, _importType.isScoped)(importPath); + +- if (!extension || !importPath.endsWith('.' + String(extension))) { ++ const validExtensions = getValidExtensionFor(context, importPath, extension); ++ if (!extension || !validExtensions.some((extension) => importPath.endsWith('.' + String(extension)))) { + // ignore type-only imports and exports +- if (node.importKind === 'type' || node.exportKind === 'type') {return;} ++ if (!props.checkTypeImports && (node.importKind === 'type' || node.exportKind === 'type')) {return;} + var extensionRequired = isUseOfExtensionRequired(extension, isPackage); + var extensionForbidden = isUseOfExtensionForbidden(extension); + if (extensionRequired && !extensionForbidden) { +@@ -190,4 +196,40 @@ module.exports = { + + return (0, _moduleVisitor2['default'])(checkFileExtension, { commonjs: true }); + }return create;}() }; ++ ++/** ++ * Taken from `eslint-import-resolver-typescript`. ++ * This could be imported from current versions of that plugin, ++ * but this project still depends on an older version. ++ * Also, importing it would add a dependency, or at least an ++ * optional peer dependency - copying the code seems like the ++ * more sane option. ++ * [LICENSE](https://github.com/import-js/eslint-import-resolver-typescript/blob/71b23a206514842fef70a99220e5ffb1d6da2a0e/LICENSE) ++ */ ++ ++const defaultExtensionAlias = { ++ '.js': [ ++ '.ts', ++ // `.tsx` can also be compiled as `.js` ++ '.tsx', ++ '.d.ts', ++ '.js', ++ ], ++ '.jsx': ['.tsx', '.d.ts', '.jsx'], ++ '.cjs': ['.cts', '.d.cts', '.cjs'], ++ '.mjs': ['.mts', '.d.mts', '.mjs'], ++}; ++ ++function getValidExtensionFor(context, importPath, resolvedExtension) { ++ let extensionAlias = {}; ++ if (context.settings['import/resolver'] && context.settings['import/resolver'].typescript) { ++ extensionAlias = context.settings['import/resolver'].typescript.extensionAlias || defaultExtensionAlias; ++ } ++ ++ const importedExtension = _path2['default'].extname(importPath); ++ if (_has2['default'](extensionAlias, importedExtension)) { ++ return extensionAlias[importedExtension].map((ext) => ext.slice(1)); ++ } ++ return [resolvedExtension]; ++} + //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9ydWxlcy9leHRlbnNpb25zLmpzIl0sIm5hbWVzIjpbImVudW1WYWx1ZXMiLCJwYXR0ZXJuUHJvcGVydGllcyIsInR5cGUiLCJwcm9wZXJ0aWVzIiwicGF0dGVybiIsImlnbm9yZVBhY2thZ2VzIiwiYnVpbGRQcm9wZXJ0aWVzIiwiY29udGV4dCIsInJlc3VsdCIsImRlZmF1bHRDb25maWciLCJvcHRpb25zIiwiZm9yRWFjaCIsIm9iaiIsInVuZGVmaW5lZCIsIk9iamVjdCIsImFzc2lnbiIsIm1vZHVsZSIsImV4cG9ydHMiLCJtZXRhIiwiZG9jcyIsImNhdGVnb3J5IiwiZGVzY3JpcHRpb24iLCJ1cmwiLCJzY2hlbWEiLCJhbnlPZiIsIml0ZW1zIiwiYWRkaXRpb25hbEl0ZW1zIiwiY3JlYXRlIiwicHJvcHMiLCJnZXRNb2RpZmllciIsImV4dGVuc2lvbiIsImlzVXNlT2ZFeHRlbnNpb25SZXF1aXJlZCIsImlzUGFja2FnZSIsImlzVXNlT2ZFeHRlbnNpb25Gb3JiaWRkZW4iLCJpc1Jlc29sdmFibGVXaXRob3V0RXh0ZW5zaW9uIiwiZmlsZSIsInBhdGgiLCJleHRuYW1lIiwiZmlsZVdpdGhvdXRFeHRlbnNpb24iLCJzbGljZSIsImxlbmd0aCIsInJlc29sdmVkRmlsZVdpdGhvdXRFeHRlbnNpb24iLCJpc0V4dGVybmFsUm9vdE1vZHVsZSIsInNsYXNoQ291bnQiLCJzcGxpdCIsImNoZWNrRmlsZUV4dGVuc2lvbiIsInNvdXJjZSIsIm5vZGUiLCJ2YWx1ZSIsImltcG9ydFBhdGhXaXRoUXVlcnlTdHJpbmciLCJzZXR0aW5ncyIsImltcG9ydFBhdGgiLCJyZXBsYWNlIiwicmVzb2x2ZWRQYXRoIiwic3Vic3RyaW5nIiwiZW5kc1dpdGgiLCJpbXBvcnRLaW5kIiwiZXhwb3J0S2luZCIsImV4dGVuc2lvblJlcXVpcmVkIiwiZXh0ZW5zaW9uRm9yYmlkZGVuIiwicmVwb3J0IiwibWVzc2FnZSIsImNvbW1vbmpzIl0sIm1hcHBpbmdzIjoiYUFBQSw0Qjs7QUFFQSxzRDtBQUNBO0FBQ0Esa0U7QUFDQSxxQzs7QUFFQSxJQUFNQSxhQUFhLEVBQUUsUUFBTSxDQUFDLFFBQUQsRUFBVyxnQkFBWCxFQUE2QixPQUE3QixDQUFSLEVBQW5CO0FBQ0EsSUFBTUMsb0JBQW9CO0FBQ3hCQyxRQUFNLFFBRGtCO0FBRXhCRCxxQkFBbUIsRUFBRSxNQUFNRCxVQUFSLEVBRkssRUFBMUI7O0FBSUEsSUFBTUcsYUFBYTtBQUNqQkQsUUFBTSxRQURXO0FBRWpCQyxjQUFZO0FBQ1ZDLGFBQVNILGlCQURDO0FBRVZJLG9CQUFnQixFQUFFSCxNQUFNLFNBQVIsRUFGTixFQUZLLEVBQW5COzs7O0FBUUEsU0FBU0ksZUFBVCxDQUF5QkMsT0FBekIsRUFBa0M7O0FBRWhDLE1BQU1DLFNBQVM7QUFDYkMsbUJBQWUsT0FERjtBQUViTCxhQUFTLEVBRkk7QUFHYkMsb0JBQWdCLEtBSEgsRUFBZjs7O0FBTUFFLFVBQVFHLE9BQVIsQ0FBZ0JDLE9BQWhCLENBQXdCLFVBQUNDLEdBQUQsRUFBUzs7QUFFL0I7QUFDQSxRQUFJLE9BQU9BLEdBQVAsS0FBZSxRQUFuQixFQUE2QjtBQUMzQkosYUFBT0MsYUFBUCxHQUF1QkcsR0FBdkI7QUFDQTtBQUNEOztBQUVEO0FBQ0EsUUFBSUEsSUFBSVIsT0FBSixLQUFnQlMsU0FBaEIsSUFBNkJELElBQUlQLGNBQUosS0FBdUJRLFNBQXhELEVBQW1FO0FBQ2pFQyxhQUFPQyxNQUFQLENBQWNQLE9BQU9KLE9BQXJCLEVBQThCUSxHQUE5QjtBQUNBO0FBQ0Q7O0FBRUQ7QUFDQSxRQUFJQSxJQUFJUixPQUFKLEtBQWdCUyxTQUFwQixFQUErQjtBQUM3QkMsYUFBT0MsTUFBUCxDQUFjUCxPQUFPSixPQUFyQixFQUE4QlEsSUFBSVIsT0FBbEM7QUFDRDs7QUFFRDtBQUNBLFFBQUlRLElBQUlQLGNBQUosS0FBdUJRLFNBQTNCLEVBQXNDO0FBQ3BDTCxhQUFPSCxjQUFQLEdBQXdCTyxJQUFJUCxjQUE1QjtBQUNEO0FBQ0YsR0F2QkQ7O0FBeUJBLE1BQUlHLE9BQU9DLGFBQVAsS0FBeUIsZ0JBQTdCLEVBQStDO0FBQzdDRCxXQUFPQyxhQUFQLEdBQXVCLFFBQXZCO0FBQ0FELFdBQU9ILGNBQVAsR0FBd0IsSUFBeEI7QUFDRDs7QUFFRCxTQUFPRyxNQUFQO0FBQ0Q7O0FBRURRLE9BQU9DLE9BQVAsR0FBaUI7QUFDZkMsUUFBTTtBQUNKaEIsVUFBTSxZQURGO0FBRUppQixVQUFNO0FBQ0pDLGdCQUFVLGFBRE47QUFFSkMsbUJBQWEsaUVBRlQ7QUFHSkMsV0FBSywwQkFBUSxZQUFSLENBSEQsRUFGRjs7O0FBUUpDLFlBQVE7QUFDTkMsYUFBTztBQUNMO0FBQ0V0QixjQUFNLE9BRFI7QUFFRXVCLGVBQU8sQ0FBQ3pCLFVBQUQsQ0FGVDtBQUdFMEIseUJBQWlCLEtBSG5CLEVBREs7O0FBTUw7QUFDRXhCLGNBQU0sT0FEUjtBQUVFdUIsZUFBTztBQUNMekIsa0JBREs7QUFFTEcsa0JBRkssQ0FGVDs7QUFNRXVCLHlCQUFpQixLQU5uQixFQU5LOztBQWNMO0FBQ0V4QixjQUFNLE9BRFI7QUFFRXVCLGVBQU8sQ0FBQ3RCLFVBQUQsQ0FGVDtBQUdFdUIseUJBQWlCLEtBSG5CLEVBZEs7O0FBbUJMO0FBQ0V4QixjQUFNLE9BRFI7QUFFRXVCLGVBQU8sQ0FBQ3hCLGlCQUFELENBRlQ7QUFHRXlCLHlCQUFpQixLQUhuQixFQW5CSzs7QUF3Qkw7QUFDRXhCLGNBQU0sT0FEUjtBQUVFdUIsZUFBTztBQUNMekIsa0JBREs7QUFFTEMseUJBRkssQ0FGVDs7QUFNRXlCLHlCQUFpQixLQU5uQixFQXhCSyxDQURELEVBUkosRUFEUzs7Ozs7O0FBOENmQyxRQTlDZSwrQkE4Q1JwQixPQTlDUSxFQThDQzs7QUFFZCxVQUFNcUIsUUFBUXRCLGdCQUFnQkMsT0FBaEIsQ0FBZDs7QUFFQSxlQUFTc0IsV0FBVCxDQUFxQkMsU0FBckIsRUFBZ0M7QUFDOUIsZUFBT0YsTUFBTXhCLE9BQU4sQ0FBYzBCLFNBQWQsS0FBNEJGLE1BQU1uQixhQUF6QztBQUNEOztBQUVELGVBQVNzQix3QkFBVCxDQUFrQ0QsU0FBbEMsRUFBNkNFLFNBQTdDLEVBQXdEO0FBQ3RELGVBQU9ILFlBQVlDLFNBQVosTUFBMkIsUUFBM0IsS0FBd0MsQ0FBQ0YsTUFBTXZCLGNBQVAsSUFBeUIsQ0FBQzJCLFNBQWxFLENBQVA7QUFDRDs7QUFFRCxlQUFTQyx5QkFBVCxDQUFtQ0gsU0FBbkMsRUFBOEM7QUFDNUMsZUFBT0QsWUFBWUMsU0FBWixNQUEyQixPQUFsQztBQUNEOztBQUVELGVBQVNJLDRCQUFULENBQXNDQyxJQUF0QyxFQUE0QztBQUMxQyxZQUFNTCxZQUFZTSxrQkFBS0MsT0FBTCxDQUFhRixJQUFiLENBQWxCO0FBQ0EsWUFBTUcsdUJBQXVCSCxLQUFLSSxLQUFMLENBQVcsQ0FBWCxFQUFjLENBQUNULFVBQVVVLE1BQXpCLENBQTdCO0FBQ0EsWUFBTUMsK0JBQStCLDBCQUFRSCxvQkFBUixFQUE4Qi9CLE9BQTlCLENBQXJDOztBQUVBLGVBQU9rQyxpQ0FBaUMsMEJBQVFOLElBQVIsRUFBYzVCLE9BQWQsQ0FBeEM7QUFDRDs7QUFFRCxlQUFTbUMsb0JBQVQsQ0FBOEJQLElBQTlCLEVBQW9DO0FBQ2xDLFlBQUlBLFNBQVMsR0FBVCxJQUFnQkEsU0FBUyxJQUE3QixFQUFtQyxDQUFFLE9BQU8sS0FBUCxDQUFlO0FBQ3BELFlBQU1RLGFBQWFSLEtBQUtTLEtBQUwsQ0FBVyxHQUFYLEVBQWdCSixNQUFoQixHQUF5QixDQUE1Qzs7QUFFQSxZQUFJRyxlQUFlLENBQW5CLEVBQXVCLENBQUUsT0FBTyxJQUFQLENBQWM7QUFDdkMsWUFBSSwwQkFBU1IsSUFBVCxLQUFrQlEsY0FBYyxDQUFwQyxFQUF1QyxDQUFFLE9BQU8sSUFBUCxDQUFjO0FBQ3ZELGVBQU8sS0FBUDtBQUNEOztBQUVELGVBQVNFLGtCQUFULENBQTRCQyxNQUE1QixFQUFvQ0MsSUFBcEMsRUFBMEM7QUFDeEM7QUFDQSxZQUFJLENBQUNELE1BQUQsSUFBVyxDQUFDQSxPQUFPRSxLQUF2QixFQUE4QixDQUFFLE9BQVM7O0FBRXpDLFlBQU1DLDRCQUE0QkgsT0FBT0UsS0FBekM7O0FBRUE7QUFDQSxZQUFJLDJCQUFVQyx5QkFBVixFQUFxQzFDLFFBQVEyQyxRQUE3QyxDQUFKLEVBQTRELENBQUUsT0FBUzs7QUFFdkUsWUFBTUMsYUFBYUYsMEJBQTBCRyxPQUExQixDQUFrQyxTQUFsQyxFQUE2QyxFQUE3QyxDQUFuQjs7QUFFQTtBQUNBO0FBQ0EsWUFBSVYscUJBQXFCUyxVQUFyQixDQUFKLEVBQXNDLENBQUUsT0FBUzs7QUFFakQsWUFBTUUsZUFBZSwwQkFBUUYsVUFBUixFQUFvQjVDLE9BQXBCLENBQXJCOztBQUVBO0FBQ0E7QUFDQSxZQUFNdUIsWUFBWU0sa0JBQUtDLE9BQUwsQ0FBYWdCLGdCQUFnQkYsVUFBN0IsRUFBeUNHLFNBQXpDLENBQW1ELENBQW5ELENBQWxCOztBQUVBO0FBQ0EsWUFBTXRCLFlBQVk7QUFDaEJtQixrQkFEZ0I7QUFFaEIsa0NBQVFBLFVBQVIsRUFBb0I1QyxPQUFwQixDQUZnQjtBQUdoQkEsZUFIZ0I7QUFJYixrQ0FBUzRDLFVBQVQsQ0FKTDs7QUFNQSxZQUFJLENBQUNyQixTQUFELElBQWMsQ0FBQ3FCLFdBQVdJLFFBQVgsY0FBd0J6QixTQUF4QixFQUFuQixFQUF5RDtBQUN2RDtBQUNBLGNBQUlpQixLQUFLUyxVQUFMLEtBQW9CLE1BQXBCLElBQThCVCxLQUFLVSxVQUFMLEtBQW9CLE1BQXRELEVBQThELENBQUUsT0FBUztBQUN6RSxjQUFNQyxvQkFBb0IzQix5QkFBeUJELFNBQXpCLEVBQW9DRSxTQUFwQyxDQUExQjtBQUNBLGNBQU0yQixxQkFBcUIxQiwwQkFBMEJILFNBQTFCLENBQTNCO0FBQ0EsY0FBSTRCLHFCQUFxQixDQUFDQyxrQkFBMUIsRUFBOEM7QUFDNUNwRCxvQkFBUXFELE1BQVIsQ0FBZTtBQUNiYixvQkFBTUQsTUFETztBQUViZTtBQUM0Qi9CLHVDQUFnQkEsU0FBaEIsV0FBZ0MsRUFENUQscUJBQ3NFbUIseUJBRHRFLE9BRmEsRUFBZjs7QUFLRDtBQUNGLFNBWkQsTUFZTyxJQUFJbkIsU0FBSixFQUFlO0FBQ3BCLGNBQUlHLDBCQUEwQkgsU0FBMUIsS0FBd0NJLDZCQUE2QmlCLFVBQTdCLENBQTVDLEVBQXNGO0FBQ3BGNUMsb0JBQVFxRCxNQUFSLENBQWU7QUFDYmIsb0JBQU1ELE1BRE87QUFFYmUscUVBQThDL0IsU0FBOUMsdUJBQWlFbUIseUJBQWpFLE9BRmEsRUFBZjs7QUFJRDtBQUNGO0FBQ0Y7O0FBRUQsYUFBTyxnQ0FBY0osa0JBQWQsRUFBa0MsRUFBRWlCLFVBQVUsSUFBWixFQUFsQyxDQUFQO0FBQ0QsS0FsSWMsbUJBQWpCIiwiZmlsZSI6ImV4dGVuc2lvbnMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcblxuaW1wb3J0IHJlc29sdmUgZnJvbSAnZXNsaW50LW1vZHVsZS11dGlscy9yZXNvbHZlJztcbmltcG9ydCB7IGlzQnVpbHRJbiwgaXNFeHRlcm5hbE1vZHVsZSwgaXNTY29wZWQgfSBmcm9tICcuLi9jb3JlL2ltcG9ydFR5cGUnO1xuaW1wb3J0IG1vZHVsZVZpc2l0b3IgZnJvbSAnZXNsaW50LW1vZHVsZS11dGlscy9tb2R1bGVWaXNpdG9yJztcbmltcG9ydCBkb2NzVXJsIGZyb20gJy4uL2RvY3NVcmwnO1xuXG5jb25zdCBlbnVtVmFsdWVzID0geyBlbnVtOiBbJ2Fsd2F5cycsICdpZ25vcmVQYWNrYWdlcycsICduZXZlciddIH07XG5jb25zdCBwYXR0ZXJuUHJvcGVydGllcyA9IHtcbiAgdHlwZTogJ29iamVjdCcsXG4gIHBhdHRlcm5Qcm9wZXJ0aWVzOiB7ICcuKic6IGVudW1WYWx1ZXMgfSxcbn07XG5jb25zdCBwcm9wZXJ0aWVzID0ge1xuICB0eXBlOiAnb2JqZWN0JyxcbiAgcHJvcGVydGllczoge1xuICAgIHBhdHRlcm46IHBhdHRlcm5Qcm9wZXJ0aWVzLFxuICAgIGlnbm9yZVBhY2thZ2VzOiB7IHR5cGU6ICdib29sZWFuJyB9LFxuICB9LFxufTtcblxuZnVuY3Rpb24gYnVpbGRQcm9wZXJ0aWVzKGNvbnRleHQpIHtcblxuICBjb25zdCByZXN1bHQgPSB7XG4gICAgZGVmYXVsdENvbmZpZzogJ25ldmVyJyxcbiAgICBwYXR0ZXJuOiB7fSxcbiAgICBpZ25vcmVQYWNrYWdlczogZmFsc2UsXG4gIH07XG5cbiAgY29udGV4dC5vcHRpb25zLmZvckVhY2goKG9iaikgPT4ge1xuXG4gICAgLy8gSWYgdGhpcyBpcyBhIHN0cmluZywgc2V0IGRlZmF1bHRDb25maWcgdG8gaXRzIHZhbHVlXG4gICAgaWYgKHR5cGVvZiBvYmogPT09ICdzdHJpbmcnKSB7XG4gICAgICByZXN1bHQuZGVmYXVsdENvbmZpZyA9IG9iajtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBJZiB0aGlzIGlzIG5vdCB0aGUgbmV3IHN0cnVjdHVyZSwgdHJhbnNmZXIgYWxsIHByb3BzIHRvIHJlc3VsdC5wYXR0ZXJuXG4gICAgaWYgKG9iai5wYXR0ZXJuID09PSB1bmRlZmluZWQgJiYgb2JqLmlnbm9yZVBhY2thZ2VzID09PSB1bmRlZmluZWQpIHtcbiAgICAgIE9iamVjdC5hc3NpZ24ocmVzdWx0LnBhdHRlcm4sIG9iaik7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgLy8gSWYgcGF0dGVybiBpcyBwcm92aWRlZCwgdHJhbnNmZXIgYWxsIHByb3BzXG4gICAgaWYgKG9iai5wYXR0ZXJuICE9PSB1bmRlZmluZWQpIHtcbiAgICAgIE9iamVjdC5hc3NpZ24ocmVzdWx0LnBhdHRlcm4sIG9iai5wYXR0ZXJuKTtcbiAgICB9XG5cbiAgICAvLyBJZiBpZ25vcmVQYWNrYWdlcyBpcyBwcm92aWRlZCwgdHJhbnNmZXIgaXQgdG8gcmVzdWx0XG4gICAgaWYgKG9iai5pZ25vcmVQYWNrYWdlcyAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICByZXN1bHQuaWdub3JlUGFja2FnZXMgPSBvYmouaWdub3JlUGFja2FnZXM7XG4gICAgfVxuICB9KTtcblxuICBpZiAocmVzdWx0LmRlZmF1bHRDb25maWcgPT09ICdpZ25vcmVQYWNrYWdlcycpIHtcbiAgICByZXN1bHQuZGVmYXVsdENvbmZpZyA9ICdhbHdheXMnO1xuICAgIHJlc3VsdC5pZ25vcmVQYWNrYWdlcyA9IHRydWU7XG4gIH1cblxuICByZXR1cm4gcmVzdWx0O1xufVxuXG5tb2R1bGUuZXhwb3J0cyA9IHtcbiAgbWV0YToge1xuICAgIHR5cGU6ICdzdWdnZXN0aW9uJyxcbiAgICBkb2NzOiB7XG4gICAgICBjYXRlZ29yeTogJ1N0eWxlIGd1aWRlJyxcbiAgICAgIGRlc2NyaXB0aW9uOiAnRW5zdXJlIGNvbnNpc3RlbnQgdXNlIG9mIGZpbGUgZXh0ZW5zaW9uIHdpdGhpbiB0aGUgaW1wb3J0IHBhdGguJyxcbiAgICAgIHVybDogZG9jc1VybCgnZXh0ZW5zaW9ucycpLFxuICAgIH0sXG5cbiAgICBzY2hlbWE6IHtcbiAgICAgIGFueU9mOiBbXG4gICAgICAgIHtcbiAgICAgICAgICB0eXBlOiAnYXJyYXknLFxuICAgICAgICAgIGl0ZW1zOiBbZW51bVZhbHVlc10sXG4gICAgICAgICAgYWRkaXRpb25hbEl0ZW1zOiBmYWxzZSxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIHR5cGU6ICdhcnJheScsXG4gICAgICAgICAgaXRlbXM6IFtcbiAgICAgICAgICAgIGVudW1WYWx1ZXMsXG4gICAgICAgICAgICBwcm9wZXJ0aWVzLFxuICAgICAgICAgIF0sXG4gICAgICAgICAgYWRkaXRpb25hbEl0ZW1zOiBmYWxzZSxcbiAgICAgICAgfSxcbiAgICAgICAge1xuICAgICAgICAgIHR5cGU6ICdhcnJheScsXG4gICAgICAgICAgaXRlbXM6IFtwcm9wZXJ0aWVzXSxcbiAgICAgICAgICBhZGRpdGlvbmFsSXRlbXM6IGZhbHNlLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgdHlwZTogJ2FycmF5JyxcbiAgICAgICAgICBpdGVtczogW3BhdHRlcm5Qcm9wZXJ0aWVzXSxcbiAgICAgICAgICBhZGRpdGlvbmFsSXRlbXM6IGZhbHNlLFxuICAgICAgICB9LFxuICAgICAgICB7XG4gICAgICAgICAgdHlwZTogJ2FycmF5JyxcbiAgICAgICAgICBpdGVtczogW1xuICAgICAgICAgICAgZW51bVZhbHVlcyxcbiAgICAgICAgICAgIHBhdHRlcm5Qcm9wZXJ0aWVzLFxuICAgICAgICAgIF0sXG4gICAgICAgICAgYWRkaXRpb25hbEl0ZW1zOiBmYWxzZSxcbiAgICAgICAgfSxcbiAgICAgIF0sXG4gICAgfSxcbiAgfSxcblxuICBjcmVhdGUoY29udGV4dCkge1xuXG4gICAgY29uc3QgcHJvcHMgPSBidWlsZFByb3BlcnRpZXMoY29udGV4dCk7XG5cbiAgICBmdW5jdGlvbiBnZXRNb2RpZmllcihleHRlbnNpb24pIHtcbiAgICAgIHJldHVybiBwcm9wcy5wYXR0ZXJuW2V4dGVuc2lvbl0gfHwgcHJvcHMuZGVmYXVsdENvbmZpZztcbiAgICB9XG5cbiAgICBmdW5jdGlvbiBpc1VzZU9mRXh0ZW5zaW9uUmVxdWlyZWQoZXh0ZW5zaW9uLCBpc1BhY2thZ2UpIHtcbiAgICAgIHJldHVybiBnZXRNb2RpZmllcihleHRlbnNpb24pID09PSAnYWx3YXlzJyAmJiAoIXByb3BzLmlnbm9yZVBhY2thZ2VzIHx8ICFpc1BhY2thZ2UpO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIGlzVXNlT2ZFeHRlbnNpb25Gb3JiaWRkZW4oZXh0ZW5zaW9uKSB7XG4gICAgICByZXR1cm4gZ2V0TW9kaWZpZXIoZXh0ZW5zaW9uKSA9PT0gJ25ldmVyJztcbiAgICB9XG5cbiAgICBmdW5jdGlvbiBpc1Jlc29sdmFibGVXaXRob3V0RXh0ZW5zaW9uKGZpbGUpIHtcbiAgICAgIGNvbnN0IGV4dGVuc2lvbiA9IHBhdGguZXh0bmFtZShmaWxlKTtcbiAgICAgIGNvbnN0IGZpbGVXaXRob3V0RXh0ZW5zaW9uID0gZmlsZS5zbGljZSgwLCAtZXh0ZW5zaW9uLmxlbmd0aCk7XG4gICAgICBjb25zdCByZXNvbHZlZEZpbGVXaXRob3V0RXh0ZW5zaW9uID0gcmVzb2x2ZShmaWxlV2l0aG91dEV4dGVuc2lvbiwgY29udGV4dCk7XG5cbiAgICAgIHJldHVybiByZXNvbHZlZEZpbGVXaXRob3V0RXh0ZW5zaW9uID09PSByZXNvbHZlKGZpbGUsIGNvbnRleHQpO1xuICAgIH1cblxuICAgIGZ1bmN0aW9uIGlzRXh0ZXJuYWxSb290TW9kdWxlKGZpbGUpIHtcbiAgICAgIGlmIChmaWxlID09PSAnLicgfHwgZmlsZSA9PT0gJy4uJykgeyByZXR1cm4gZmFsc2U7IH1cbiAgICAgIGNvbnN0IHNsYXNoQ291bnQgPSBmaWxlLnNwbGl0KCcvJykubGVuZ3RoIC0gMTtcblxuICAgICAgaWYgKHNsYXNoQ291bnQgPT09IDApICB7IHJldHVybiB0cnVlOyB9XG4gICAgICBpZiAoaXNTY29wZWQoZmlsZSkgJiYgc2xhc2hDb3VudCA8PSAxKSB7IHJldHVybiB0cnVlOyB9XG4gICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuXG4gICAgZnVuY3Rpb24gY2hlY2tGaWxlRXh0ZW5zaW9uKHNvdXJjZSwgbm9kZSkge1xuICAgICAgLy8gYmFpbCBpZiB0aGUgZGVjbGFyYXRpb24gZG9lc24ndCBoYXZlIGEgc291cmNlLCBlLmcuIFwiZXhwb3J0IHsgZm9vIH07XCIsIG9yIGlmIGl0J3Mgb25seSBwYXJ0aWFsbHkgdHlwZWQgbGlrZSBpbiBhbiBlZGl0b3JcbiAgICAgIGlmICghc291cmNlIHx8ICFzb3VyY2UudmFsdWUpIHsgcmV0dXJuOyB9XG5cbiAgICAgIGNvbnN0IGltcG9ydFBhdGhXaXRoUXVlcnlTdHJpbmcgPSBzb3VyY2UudmFsdWU7XG5cbiAgICAgIC8vIGRvbid0IGVuZm9yY2UgYW55dGhpbmcgb24gYnVpbHRpbnNcbiAgICAgIGlmIChpc0J1aWx0SW4oaW1wb3J0UGF0aFdpdGhRdWVyeVN0cmluZywgY29udGV4dC5zZXR0aW5ncykpIHsgcmV0dXJuOyB9XG5cbiAgICAgIGNvbnN0IGltcG9ydFBhdGggPSBpbXBvcnRQYXRoV2l0aFF1ZXJ5U3RyaW5nLnJlcGxhY2UoL1xcPyguKikkLywgJycpO1xuXG4gICAgICAvLyBkb24ndCBlbmZvcmNlIGluIHJvb3QgZXh0ZXJuYWwgcGFja2FnZXMgYXMgdGhleSBtYXkgaGF2ZSBuYW1lcyB3aXRoIGAuanNgLlxuICAgICAgLy8gTGlrZSBgaW1wb3J0IERlY2ltYWwgZnJvbSBkZWNpbWFsLmpzYClcbiAgICAgIGlmIChpc0V4dGVybmFsUm9vdE1vZHVsZShpbXBvcnRQYXRoKSkgeyByZXR1cm47IH1cblxuICAgICAgY29uc3QgcmVzb2x2ZWRQYXRoID0gcmVzb2x2ZShpbXBvcnRQYXRoLCBjb250ZXh0KTtcblxuICAgICAgLy8gZ2V0IGV4dGVuc2lvbiBmcm9tIHJlc29sdmVkIHBhdGgsIGlmIHBvc3NpYmxlLlxuICAgICAgLy8gZm9yIHVucmVzb2x2ZWQsIHVzZSBzb3VyY2UgdmFsdWUuXG4gICAgICBjb25zdCBleHRlbnNpb24gPSBwYXRoLmV4dG5hbWUocmVzb2x2ZWRQYXRoIHx8IGltcG9ydFBhdGgpLnN1YnN0cmluZygxKTtcblxuICAgICAgLy8gZGV0ZXJtaW5lIGlmIHRoaXMgaXMgYSBtb2R1bGVcbiAgICAgIGNvbnN0IGlzUGFja2FnZSA9IGlzRXh0ZXJuYWxNb2R1bGUoXG4gICAgICAgIGltcG9ydFBhdGgsXG4gICAgICAgIHJlc29sdmUoaW1wb3J0UGF0aCwgY29udGV4dCksXG4gICAgICAgIGNvbnRleHQsXG4gICAgICApIHx8IGlzU2NvcGVkKGltcG9ydFBhdGgpO1xuXG4gICAgICBpZiAoIWV4dGVuc2lvbiB8fCAhaW1wb3J0UGF0aC5lbmRzV2l0aChgLiR7ZXh0ZW5zaW9ufWApKSB7XG4gICAgICAgIC8vIGlnbm9yZSB0eXBlLW9ubHkgaW1wb3J0cyBhbmQgZXhwb3J0c1xuICAgICAgICBpZiAobm9kZS5pbXBvcnRLaW5kID09PSAndHlwZScgfHwgbm9kZS5leHBvcnRLaW5kID09PSAndHlwZScpIHsgcmV0dXJuOyB9XG4gICAgICAgIGNvbnN0IGV4dGVuc2lvblJlcXVpcmVkID0gaXNVc2VPZkV4dGVuc2lvblJlcXVpcmVkKGV4dGVuc2lvbiwgaXNQYWNrYWdlKTtcbiAgICAgICAgY29uc3QgZXh0ZW5zaW9uRm9yYmlkZGVuID0gaXNVc2VPZkV4dGVuc2lvbkZvcmJpZGRlbihleHRlbnNpb24pO1xuICAgICAgICBpZiAoZXh0ZW5zaW9uUmVxdWlyZWQgJiYgIWV4dGVuc2lvbkZvcmJpZGRlbikge1xuICAgICAgICAgIGNvbnRleHQucmVwb3J0KHtcbiAgICAgICAgICAgIG5vZGU6IHNvdXJjZSxcbiAgICAgICAgICAgIG1lc3NhZ2U6XG4gICAgICAgICAgICAgIGBNaXNzaW5nIGZpbGUgZXh0ZW5zaW9uICR7ZXh0ZW5zaW9uID8gYFwiJHtleHRlbnNpb259XCIgYCA6ICcnfWZvciBcIiR7aW1wb3J0UGF0aFdpdGhRdWVyeVN0cmluZ31cImAsXG4gICAgICAgICAgfSk7XG4gICAgICAgIH1cbiAgICAgIH0gZWxzZSBpZiAoZXh0ZW5zaW9uKSB7XG4gICAgICAgIGlmIChpc1VzZU9mRXh0ZW5zaW9uRm9yYmlkZGVuKGV4dGVuc2lvbikgJiYgaXNSZXNvbHZhYmxlV2l0aG91dEV4dGVuc2lvbihpbXBvcnRQYXRoKSkge1xuICAgICAgICAgIGNvbnRleHQucmVwb3J0KHtcbiAgICAgICAgICAgIG5vZGU6IHNvdXJjZSxcbiAgICAgICAgICAgIG1lc3NhZ2U6IGBVbmV4cGVjdGVkIHVzZSBvZiBmaWxlIGV4dGVuc2lvbiBcIiR7ZXh0ZW5zaW9ufVwiIGZvciBcIiR7aW1wb3J0UGF0aFdpdGhRdWVyeVN0cmluZ31cImAsXG4gICAgICAgICAgfSk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gbW9kdWxlVmlzaXRvcihjaGVja0ZpbGVFeHRlbnNpb24sIHsgY29tbW9uanM6IHRydWUgfSk7XG4gIH0sXG59O1xuIl19 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c317a34a..373d56da3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: apps/website: dependencies: + '@discordjs/api-extractor-model': + specifier: workspace:^ + version: link:../../packages/api-extractor-model '@discordjs/api-extractor-utils': specifier: workspace:^ version: link:../../packages/api-extractor-utils @@ -238,9 +241,6 @@ importers: '@discordjs/ui': specifier: workspace:^ version: link:../../packages/ui - '@microsoft/api-extractor-model': - specifier: ^7.28.2 - version: 7.28.2(@types/node@18.18.8) '@microsoft/tsdoc': specifier: ^0.14.2 version: 0.14.2 @@ -439,11 +439,136 @@ importers: specifier: ^0.34.6 version: 0.34.6(happy-dom@12.10.3) + packages/api-extractor: + dependencies: + '@discordjs/api-extractor-model': + specifier: workspace:^ + version: link:../api-extractor-model + '@microsoft/tsdoc': + specifier: 0.14.2 + version: 0.14.2 + '@microsoft/tsdoc-config': + specifier: 0.16.2 + version: 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) + '@rushstack/node-core-library': + specifier: 3.61.0 + version: 3.61.0(@types/node@18.18.8) + '@rushstack/rig-package': + specifier: 0.5.1 + version: 0.5.1 + '@rushstack/ts-command-line': + specifier: 4.17.1 + version: 4.17.1 + colors: + specifier: ~1.2.1 + version: 1.2.5 + lodash: + specifier: ~4.17.15 + version: 4.17.21 + resolve: + specifier: ~1.22.1 + version: 1.22.8 + semver: + specifier: ~7.5.4 + version: 7.5.4 + source-map: + specifier: ~0.6.1 + version: 0.6.1 + typescript: + specifier: ^5.2.2 + version: 5.2.2 + devDependencies: + '@types/jest': + specifier: ^29.5.7 + version: 29.5.7 + '@types/lodash': + specifier: ^4.14.200 + version: 4.14.200 + '@types/node': + specifier: ^18.18.8 + version: 18.18.8 + '@types/resolve': + specifier: ^1.20.4 + version: 1.20.4 + '@types/semver': + specifier: ^7.5.0 + version: 7.5.4 + cpy-cli: + specifier: ^5.0.0 + version: 5.0.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ^8.53.0 + version: 8.53.0 + eslint-config-neon: + specifier: ^0.1.57 + version: 0.1.57(eslint@8.53.0)(typescript@5.2.2) + eslint-formatter-pretty: + specifier: ^5.0.0 + version: 5.0.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.8) + prettier: + specifier: ^3.0.3 + version: 3.0.3 + tsup: + specifier: ^7.2.0 + version: 7.2.0(typescript@5.2.2) + turbo: + specifier: ^1.10.16 + version: 1.10.16 + + packages/api-extractor-model: + dependencies: + '@microsoft/tsdoc': + specifier: 0.14.2 + version: 0.14.2 + '@microsoft/tsdoc-config': + specifier: 0.16.2 + version: 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) + '@rushstack/node-core-library': + specifier: 3.61.0 + version: 3.61.0(@types/node@18.18.8) + devDependencies: + '@types/jest': + specifier: ^29.5.7 + version: 29.5.7 + '@types/node': + specifier: ^18.18.8 + version: 18.18.8 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ^8.53.0 + version: 8.53.0 + eslint-config-neon: + specifier: ^0.1.57 + version: 0.1.57(eslint@8.53.0)(typescript@5.2.2) + eslint-formatter-pretty: + specifier: ^5.0.0 + version: 5.0.0 + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@18.18.8) + prettier: + specifier: ^3.0.3 + version: 3.0.3 + tsup: + specifier: ^7.2.0 + version: 7.2.0(typescript@5.2.2) + turbo: + specifier: ^1.10.16 + version: 1.10.16 + packages/api-extractor-utils: dependencies: - '@microsoft/api-extractor-model': - specifier: 7.28.2 - version: 7.28.2(@types/node@16.18.60) + '@discordjs/api-extractor-model': + specifier: workspace:^ + version: link:../api-extractor-model '@microsoft/tsdoc': specifier: 0.14.2 version: 0.14.2 @@ -488,12 +613,12 @@ importers: specifier: ^5.3.2 version: 5.3.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -552,12 +677,12 @@ importers: specifier: ^2.6.2 version: 2.6.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -600,12 +725,12 @@ importers: packages/collection: devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -664,12 +789,12 @@ importers: specifier: 0.37.61 version: 0.37.61 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@18.18.8) '@types/node': specifier: 18.18.8 version: 18.18.8 @@ -725,12 +850,12 @@ importers: specifier: ^5.0.0 version: 5.0.0 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -911,12 +1036,12 @@ importers: specifier: 0.37.61 version: 0.37.61 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -978,12 +1103,12 @@ importers: specifier: 0.37.61 version: 0.37.61 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@18.18.8) '@types/node': specifier: 18.18.8 version: 18.18.8 @@ -1036,12 +1161,12 @@ importers: specifier: 5.27.2 version: 5.27.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@18.18.8) '@types/node': specifier: 18.18.8 version: 18.18.8 @@ -1152,12 +1277,12 @@ importers: specifier: 5.27.2 version: 5.27.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@18.17.9) '@types/node': specifier: 18.17.9 version: 18.17.9 @@ -1197,12 +1322,12 @@ importers: packages/scripts: dependencies: + '@discordjs/api-extractor-model': + specifier: workspace:^ + version: link:../api-extractor-model '@discordjs/api-extractor-utils': specifier: workspace:^ version: link:../api-extractor-utils - '@microsoft/api-extractor-model': - specifier: 7.28.2 - version: 7.28.2(@types/node@16.18.60) '@microsoft/tsdoc': specifier: 0.14.2 version: 0.14.2 @@ -1364,12 +1489,12 @@ importers: packages/util: devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/node': specifier: 16.18.60 version: 16.18.60 @@ -1434,12 +1559,12 @@ importers: '@babel/preset-typescript': specifier: ^7.23.2 version: 7.23.2(@babel/core@7.23.2) + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@16.18.60) '@types/jest': specifier: ^29.5.7 version: 29.5.7 @@ -1516,12 +1641,12 @@ importers: specifier: ^8.14.2 version: 8.14.2 devDependencies: + '@discordjs/api-extractor': + specifier: workspace:^ + version: link:../api-extractor '@favware/cliff-jumper': specifier: ^2.2.1 version: 2.2.1 - '@microsoft/api-extractor': - specifier: ^7.38.2 - version: 7.38.2(@types/node@18.17.9) '@types/node': specifier: 18.17.9 version: 18.17.9 @@ -4264,25 +4389,6 @@ packages: transitivePeerDependencies: - '@types/node' - /@microsoft/api-extractor-model@7.28.2(@types/node@18.17.9): - resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) - '@rushstack/node-core-library': 3.61.0(@types/node@18.17.9) - transitivePeerDependencies: - - '@types/node' - dev: true - - /@microsoft/api-extractor-model@7.28.2(@types/node@18.18.8): - resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) - '@rushstack/node-core-library': 3.61.0(@types/node@18.18.8) - transitivePeerDependencies: - - '@types/node' - /@microsoft/api-extractor@7.38.2(@types/node@16.18.60): resolution: {integrity: sha512-JOARuhTwOcOMIU0O2czscoJy3ddVzIRhSA9/7T1ALuZSNphgWsPk+Bv4E7AnBDmTV4pP4lBNLtCxEHjjpWaytQ==} hasBin: true @@ -4303,46 +4409,6 @@ packages: - '@types/node' dev: true - /@microsoft/api-extractor@7.38.2(@types/node@18.17.9): - resolution: {integrity: sha512-JOARuhTwOcOMIU0O2czscoJy3ddVzIRhSA9/7T1ALuZSNphgWsPk+Bv4E7AnBDmTV4pP4lBNLtCxEHjjpWaytQ==} - hasBin: true - dependencies: - '@microsoft/api-extractor-model': 7.28.2(@types/node@18.17.9) - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) - '@rushstack/node-core-library': 3.61.0(@types/node@18.17.9) - '@rushstack/rig-package': 0.5.1 - '@rushstack/ts-command-line': 4.17.1 - colors: 1.2.5 - lodash: 4.17.21 - resolve: 1.22.8 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.0.4 - transitivePeerDependencies: - - '@types/node' - dev: true - - /@microsoft/api-extractor@7.38.2(@types/node@18.18.8): - resolution: {integrity: sha512-JOARuhTwOcOMIU0O2czscoJy3ddVzIRhSA9/7T1ALuZSNphgWsPk+Bv4E7AnBDmTV4pP4lBNLtCxEHjjpWaytQ==} - hasBin: true - dependencies: - '@microsoft/api-extractor-model': 7.28.2(@types/node@18.18.8) - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu) - '@rushstack/node-core-library': 3.61.0(@types/node@18.18.8) - '@rushstack/rig-package': 0.5.1 - '@rushstack/ts-command-line': 4.17.1 - colors: 1.2.5 - lodash: 4.17.21 - resolve: 1.22.8 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.0.4 - transitivePeerDependencies: - - '@types/node' - dev: true - /@microsoft/tsdoc-config@0.16.2(patch_hash=35av6rrndvjtr2u2jso66jatbu): resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} dependencies: @@ -5638,24 +5704,6 @@ packages: semver: 7.5.4 z-schema: 5.0.5 - /@rushstack/node-core-library@3.61.0(@types/node@18.17.9): - resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - dependencies: - '@types/node': 18.17.9 - colors: 1.2.5 - fs-extra: 7.0.1 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.8 - semver: 7.5.4 - z-schema: 5.0.5 - dev: true - /@rushstack/node-core-library@3.61.0(@types/node@18.18.8): resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==} peerDependencies: @@ -5678,7 +5726,6 @@ packages: dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - dev: true /@rushstack/ts-command-line@4.17.1: resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==} @@ -5687,7 +5734,6 @@ packages: argparse: 1.0.10 colors: 1.2.5 string-argv: 0.3.2 - dev: true /@sapphire/async-queue@1.5.0: resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} @@ -6910,7 +6956,6 @@ packages: /@types/argparse@1.0.38: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - dev: true /@types/aria-query@5.0.3: resolution: {integrity: sha512-0Z6Tr7wjKJIk4OUEjVUQMtyunLDy339vcMaj38Kpj6jM2OE1p3S4kXExKZ7a3uXQAPCoy3sbrP1wibDKaf39oA==} @@ -7275,6 +7320,10 @@ packages: /@types/scheduler@0.16.5: resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==} + /@types/semver@7.5.0: + resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} + dev: true + /@types/semver@7.5.4: resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} dev: true @@ -9613,6 +9662,7 @@ packages: engines: {node: '>=0.10.0'} requiresBuild: true dev: true + optional: true /collect-all@1.0.4: resolution: {integrity: sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA==} @@ -10199,6 +10249,25 @@ packages: - ts-node dev: true + /create-jest@29.7.0(@types/node@18.18.8): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + 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@18.18.8) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -10382,7 +10451,7 @@ packages: supports-color: optional: true dependencies: - ms: 2.1.1 + ms: 2.1.3 dev: true /debug@4.3.4: @@ -13860,6 +13929,7 @@ packages: dependencies: number-is-nan: 1.0.1 dev: true + optional: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -14279,6 +14349,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@18.18.8): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.18.8) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.18.8) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@29.7.0(@types/node@16.18.60): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -14678,6 +14776,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@18.18.8): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.18.8) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true @@ -16796,6 +16915,7 @@ packages: engines: {node: '>=0.10.0'} requiresBuild: true dev: true + optional: true /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} @@ -19302,7 +19422,6 @@ packages: /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - dev: true /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -19321,6 +19440,7 @@ packages: is-fullwidth-code-point: 1.0.0 strip-ansi: 3.0.1 dev: true + optional: true /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -21545,7 +21665,7 @@ packages: /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} dependencies: - string-width: 1.0.2 + string-width: 4.2.3 dev: true /wordwrap@1.0.0: diff --git a/turbo.json b/turbo.json index b790c2572..8f6c99b49 100644 --- a/turbo.json +++ b/turbo.json @@ -353,7 +353,6 @@ "../../tsconfig.docs.json", "../../tsconfig.json", "api-extractor.json", - "api-extractor-docs.json", "docs/**/*", "!docs/docs.json", "!docs/docs.api.json",