diff --git a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts index 5e50629da..a32fe779e 100644 --- a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts +++ b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts @@ -2,7 +2,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { TSDocConfiguration } from '@microsoft/tsdoc'; +import { + type DocNode, + type DocPlainText, + DocDeclarationReference, + DocNodeKind, + TSDocConfiguration, + DocMemberReference, + DocMemberIdentifier, +} from '@microsoft/tsdoc'; import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js'; import { InternalError } from '@rushstack/node-core-library'; import type { IExcerptToken, IExcerptTokenRange } from '../index.js'; @@ -42,6 +50,11 @@ export interface IApiItemContainerJson extends IApiItemJson { preserveMemberOrder?: boolean; } +interface Mixin { + declarationReference: DocDeclarationReference; + typeParameters: IExcerptTokenRange[]; +} + interface ExcerptTokenRangeInDeclaredItem { item: ApiDeclaredItem; range: IExcerptTokenRange; @@ -393,14 +406,52 @@ export function ApiItemContainerMixin( } } + const findPlainTextNode = (node: DocNode): DocPlainText | undefined => { + switch (node.kind) { + case DocNodeKind.PlainText: + return node as DocPlainText; + default: + for (const child of node.getChildNodes()) { + const result = findPlainTextNode(child); + if (result) return result; + } + } + + return undefined; + }; + // Interfaces can extend multiple interfaces, so iterate through all of them. + // Also Classes can have multiple mixins const extendedItems: IMappedTypeParameters[] = []; - let extendsTypes: readonly HeritageType[] | undefined; + let extendsTypes: readonly (HeritageType | Mixin)[] | undefined; switch (next.item.kind) { case ApiItemKind.Class: { const apiClass: ApiClass = next.item as ApiClass; - extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : []; + const configuration = apiClass.tsdocComment?.configuration ?? new TSDocConfiguration(); + const mixins = + apiClass.tsdocComment?.customBlocks + .filter( + (block) => block.blockTag.tagName === '@mixes', // && + // block.getChildNodes().some((node) => node.kind === DocNodeKind.PlainText), + ) + .map(findPlainTextNode) + .filter((block) => block !== undefined) + .map((block) => ({ + declarationReference: new DocDeclarationReference({ + configuration, + memberReferences: block.text.split('.').map( + (part, index) => + new DocMemberReference({ + configuration, + hasDot: index > 0, + memberIdentifier: new DocMemberIdentifier({ configuration, identifier: part }), + }), + ), + }), + typeParameters: [] as IExcerptTokenRange[], + })) ?? []; + extendsTypes = apiClass.extendsType ? [apiClass.extendsType, ...mixins] : [...mixins]; break; } @@ -425,30 +476,38 @@ export function ApiItemContainerMixin( } 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; - }, - ); + let canonicalReference: DeclarationReference | DocDeclarationReference; + if ('excerpt' in extendsType) { + // 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.item.displayName} because no canonical reference was found`, - }); - maybeIncompleteResult = true; - continue; + if (!firstReferenceToken) { + messages.push({ + messageId: FindApiItemsMessageId.ExtendsClauseMissingReference, + text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`, + }); + maybeIncompleteResult = true; + continue; + } + + canonicalReference = firstReferenceToken.canonicalReference!; + } else { + // extendsType is a Mixin + canonicalReference = extendsType.declarationReference; } const apiModel: ApiModel | undefined = this.getAssociatedModel(); @@ -461,10 +520,9 @@ export function ApiItemContainerMixin( continue; } - const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!; const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference( canonicalReference, - undefined, + this, ); const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem; diff --git a/packages/api-extractor-model/src/model/ApiPackage.ts b/packages/api-extractor-model/src/model/ApiPackage.ts index 3404d5342..fee2211a6 100644 --- a/packages/api-extractor-model/src/model/ApiPackage.ts +++ b/packages/api-extractor-model/src/model/ApiPackage.ts @@ -293,7 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration(); - if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfiguration' in jsonObject) { + if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfig' in jsonObject.metadata) { const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig); if (tsdocConfigFile.hasErrors) { throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary()); @@ -420,6 +420,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented for (const key of Object.keys(item)) { if (key === 'dependencies') { result[MinifyJSONMapping.dependencies] = item.dependencies; + } else if (key === 'tsdocConfig') { + result[MinifyJSONMapping.tsdocConfig] = item.tsdocConfig; } else result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] = typeof item[key] === 'object' ? mapper(item[key]) : item[key]; @@ -440,6 +442,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented for (const key of Object.keys(item)) { if (key === MinifyJSONMapping.dependencies) { result.dependencies = item[MinifyJSONMapping.dependencies]; + } else if (key === MinifyJSONMapping.tsdocConfig) { + result.tsdocConfig = item[MinifyJSONMapping.tsdocConfig]; } else result[ Object.keys(MinifyJSONMapping).find( diff --git a/packages/api-extractor/extends/tsdoc-base.json b/packages/api-extractor/extends/tsdoc-base.json index 38dce1501..c5a78650b 100644 --- a/packages/api-extractor/extends/tsdoc-base.json +++ b/packages/api-extractor/extends/tsdoc-base.json @@ -32,6 +32,10 @@ { "tagName": "@preapproved", "syntaxKind": "modifier" + }, + { + "tagName": "@mixes", + "syntaxKind": "block" } ], diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts index 14a600dac..82cd54820 100644 --- a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts @@ -9,6 +9,11 @@ import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js /** * A builder that creates API-compatible JSON data for chat input commands. + * + * @mixes CommandBuilder + * @mixes SharedChatInputCommandOptions + * @mixes SharedNameAndDescription + * @mixes SharedChatInputCommandSubcommands */ export class ChatInputCommandBuilder extends Mixin( CommandBuilder, diff --git a/packages/builders/tsdoc.json b/packages/builders/tsdoc.json new file mode 100644 index 000000000..92aaafe6f --- /dev/null +++ b/packages/builders/tsdoc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "tagDefinitions": [ + { + "tagName": "@mixes", + "syntaxKind": "block" + } + ] +}