From 59635bba34fd65c430c009ac0f16ef744482d48e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sun, 2 Nov 2025 22:24:58 -0800 Subject: [PATCH] docs: improve docs generation for namespaces (#64747) Improves docs epxerience for interfaces / type aliases that are merged with a namespace. PR Close #64747 --- .../docs-viewer/docs-viewer.component.ts | 24 +++++---- .../pipeline/api-gen/rendering/entities.mts | 10 +++- .../rendering/entities/categorization.mts | 7 ++- .../rendering/entities/renderables.mts | 3 +- .../rendering/templates/class-member.tsx | 15 +++++- .../templates/code-table-of-contents.tsx | 19 +++++-- .../rendering/transforms/class-transforms.mts | 3 +- .../rendering/transforms/code-transforms.mts | 17 ++++-- .../transforms/decorator-transforms.mts | 1 - .../rendering/transforms/enum-transforms.mts | 3 +- .../transforms/interface-transforms.mts | 3 +- .../rendering/transforms/jsdoc-transforms.mts | 8 +-- .../transforms/member-transforms.mts | 52 ++++++++++++++----- .../transforms/type-alias-transforms.mts | 29 +++++------ adev/shared-docs/styles/docs/_code.scss | 7 ++- .../src/ngtsc/docs/src/entities.ts | 10 +++- .../src/ngtsc/docs/src/extractor.ts | 24 ++++++++- .../ngtsc/docs/src/type_alias_extractor.ts | 3 +- 18 files changed, 174 insertions(+), 64 deletions(-) diff --git a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts index 964af1d478b..12ce49f5eab 100644 --- a/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts +++ b/adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts @@ -14,32 +14,32 @@ import { ComponentRef, createComponent, DestroyRef, + effect, ElementRef, EnvironmentInjector, inject, Injector, + input, + output, + PendingTasks, PLATFORM_ID, Type, ViewContainerRef, ViewEncapsulation, - PendingTasks, - output, - input, - effect, } from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {TOC_SKIP_CONTENT_MARKER, NavigationState} from '../../../services'; -import {TableOfContents} from '../../table-of-contents/table-of-contents.component'; -import {IconComponent} from '../../icon/icon.component'; -import {handleHrefClickEventWithRouter} from '../../../utils'; -import {Snippet} from '../../../interfaces'; import {Router} from '@angular/router'; import {fromEvent} from 'rxjs'; +import {Snippet} from '../../../interfaces'; +import {NavigationState, TOC_SKIP_CONTENT_MARKER} from '../../../services'; +import {handleHrefClickEventWithRouter} from '../../../utils'; +import {IconComponent} from '../../icon/icon.component'; +import {TableOfContents} from '../../table-of-contents/table-of-contents.component'; +import {DomSanitizer} from '@angular/platform-browser'; import {Breadcrumb} from '../../breadcrumb/breadcrumb.component'; import {CopySourceCodeButton} from '../../copy-source-code-button/copy-source-code-button.component'; import {ExampleViewer} from '../example-viewer/example-viewer.component'; -import {DomSanitizer} from '@angular/platform-browser'; const TOC_HOST_ELEMENT_NAME = 'docs-table-of-contents'; export const ASSETS_EXAMPLES_PATH = 'assets/content/examples'; @@ -253,7 +253,9 @@ export class DocViewer { // the code private loadCopySourceCodeButtons(): void { const staticCodeSnippets = ( - Array.from(this.elementRef.nativeElement.querySelectorAll('.docs-code:not([mermaid])')) + Array.from( + this.elementRef.nativeElement.querySelectorAll('.docs-code:not([mermaid],.docs-no-copy)'), + ) ); for (let codeSnippet of staticCodeSnippets) { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities.mts b/adev/shared-docs/pipeline/api-gen/rendering/entities.mts index f0e1bf60534..976e1beae69 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities.mts @@ -35,6 +35,8 @@ export enum MemberType { Getter = 'getter', Setter = 'setter', EnumItem = 'enum_item', + Interface = 'interface', + TypeAlias = 'type_alias', } export enum DecoratorType { @@ -96,7 +98,7 @@ export interface ConstantEntry extends DocEntry { /** Documentation entity for a type alias. */ export interface TypeAliasEntry extends ConstantEntry { generics: GenericEntry[]; - members?: MemberEntry[]; // For merged namespaces + members: MemberEntry[]; // For merged namespaces } /** Documentation entity for a TypeScript class. */ @@ -192,6 +194,12 @@ export interface PropertyEntry extends MemberEntry { /** Sub-entry for a class method. */ export type MethodEntry = MemberEntry & FunctionEntry; +/** Sub-entry for an interface (included via namespace). */ +export type InterfaceMemberEntry = MemberEntry & InterfaceEntry; + +/** Sub-entry for a type alias (included via namespace). */ +export type TypeAliasMemberEntry = MemberEntry & TypeAliasEntry; + /** Sub-entry for a single function parameter. */ export interface ParameterEntry { name: string; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.mts b/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.mts index bd08dcc4d6e..a934398c533 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities/categorization.mts @@ -84,10 +84,13 @@ export function isEnumEntry(entry: DocEntry): entry is EnumEntry { } /** Gets whether the given entry represents an interface. */ +export function isInterfaceEntry( + entry: MemberEntryRenderable, +): entry is InterfaceEntryRenderable & MemberEntryRenderable; export function isInterfaceEntry(entry: DocEntryRenderable): entry is InterfaceEntryRenderable; export function isInterfaceEntry(entry: DocEntry): entry is InterfaceEntry; -export function isInterfaceEntry(entry: DocEntry): entry is InterfaceEntry { - return entry.entryType === EntryType.Interface; +export function isInterfaceEntry(entry: DocEntry | MemberEntryRenderable): entry is InterfaceEntry { + return (entry as DocEntry).entryType === EntryType.Interface; } /** Gets whether the given member entry is a method entry. */ diff --git a/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.mts b/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.mts index 4c9a01f534a..b5a23c43ea6 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/entities/renderables.mts @@ -59,7 +59,7 @@ export type ConstantEntryRenderable = ConstantEntry & export type TypeAliasEntryRenderable = TypeAliasEntry & DocEntryRenderable & HasRenderableToc & { - members?: MemberEntryRenderable[]; + members: MemberEntryRenderable[]; }; /** Documentation entity for a TypeScript class augmented transformed content for rendering. */ @@ -108,6 +108,7 @@ export interface MemberEntryRenderable extends MemberEntry { jsdocTags: JsDocTagRenderable[]; deprecationMessage: string | null; htmlUsageNotes: string; + displayName?: string; } /** Sub-entry for a class method augmented transformed content for rendering. */ diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx index c2d0548c56b..fc64037aea4 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/class-member.tsx @@ -7,9 +7,11 @@ */ import {Fragment, h} from 'preact'; +import {MemberType} from '../entities.mjs'; import { isClassMethodEntry, isGetterEntry, + isInterfaceEntry, isPropertyEntry, isSetterEntry, } from '../entities/categorization.mjs'; @@ -23,6 +25,7 @@ import { import {getFunctionMetadataRenderable} from '../transforms/function-transforms.mjs'; import {ClassMethodInfo} from './class-method-info'; import {CodeSymbol} from './code-symbols'; +import {CodeTableOfContents} from './code-table-of-contents'; import {DeprecatedLabel} from './deprecated-label'; import {RawHtml} from './raw-html'; @@ -53,11 +56,12 @@ export function ClassMember(props: {member: MemberEntryRenderable}) { ); const memberName = member.name; + const displayName = member.displayName; const returnType = getMemberType(member); return (
-

{memberName}

+

{displayName ?? memberName}

{isClassMethodEntry(member) && member.signatures.length > 1 ? ( {member.signatures.length} overloads ) : returnType ? ( @@ -67,6 +71,11 @@ export function ClassMember(props: {member: MemberEntryRenderable}) { )}
{body} + {isInterfaceEntry(member) ? ( + + ) : ( + <> + )}
); } @@ -76,6 +85,10 @@ function getMemberType(entry: MemberEntryRenderable): string | null { return entry.implementation.returnType; } else if (isPropertyEntry(entry) || isGetterEntry(entry) || isSetterEntry(entry)) { return entry.type; + } else if (entry.memberType === MemberType.TypeAlias) { + return 'type'; + } else if (entry.memberType === MemberType.Interface) { + return 'interface'; } return null; } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/templates/code-table-of-contents.tsx b/adev/shared-docs/pipeline/api-gen/rendering/templates/code-table-of-contents.tsx index f360bee438a..e82a745dfc4 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/templates/code-table-of-contents.tsx +++ b/adev/shared-docs/pipeline/api-gen/rendering/templates/code-table-of-contents.tsx @@ -8,10 +8,14 @@ import {h} from 'preact'; import {renderToString} from 'preact-render-to-string'; -import {CodeLineGroup} from './code-line-group'; import {HasRenderableToc} from '../entities/traits.mjs'; +import {CodeLineGroup} from './code-line-group'; -export function CodeTableOfContents(props: {entry: HasRenderableToc}) { +export function CodeTableOfContents(props: { + entry: HasRenderableToc; + hideCopyButton?: boolean; + embedded?: boolean; +}) { let html: string; // Prefer the formatted code if available if (props.entry.formattedCode) { @@ -26,5 +30,14 @@ export function CodeTableOfContents(props: {entry: HasRenderableToc}) { ${props.entry.afterCodeGroups}`; } - return
; + return ( +
+ ); } diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/class-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/class-transforms.mts index bca29e6b06f..805d63f23a7 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/class-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/class-transforms.mts @@ -28,7 +28,7 @@ export async function getClassRenderable( ): Promise { return setEntryFlags( await addRenderableCodeToc( - addRenderableMembers( + await addRenderableMembers( addHtmlAdditionalLinks( addHtmlUsageNotes( addHtmlJsDocTagComments( @@ -36,6 +36,7 @@ export async function getClassRenderable( ), ), ), + classEntry.name, ), ), ); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.mts index b11ca89b352..299019b943a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/code-transforms.mts @@ -7,12 +7,12 @@ */ import { + DecoratorEntry, DocEntry, EntryType, FunctionSignatureMetadata, GenericEntry, MemberEntry, - DecoratorEntry, MemberTags, ParameterEntry, PropertyEntry, @@ -34,16 +34,20 @@ import { } from '../entities/categorization.mjs'; import {CodeLineRenderable} from '../entities/renderables.mjs'; import {HasModuleName, HasRenderableToc} from '../entities/traits.mjs'; -import {getSymbolUrl} from '../symbol-context.mjs'; import { getHighlighterInstance, insertParenthesesForDecoratorInShikiHtml, replaceKeywordFromShikiHtml, } from '../shiki/shiki.mjs'; +import {getSymbolUrl} from '../symbol-context.mjs'; -import {filterLifecycleMethods, mergeGettersAndSetters} from './member-transforms.mjs'; -import {formatJs} from './format-code.mjs'; import {codeToHtml} from '../../../shared/shiki.mjs'; +import {formatJs} from './format-code.mjs'; +import { + filterLifecycleMethods, + filterMergedNamespaceMembers, + mergeGettersAndSetters, +} from './member-transforms.mjs'; const INDENT = ' '; const SPACE = ' '; @@ -198,7 +202,10 @@ function mapDocEntryToCode(entry: DocEntry): CodeTableOfContentsData { } if (isInterfaceEntry(entry)) { - return getCodeTocData(mergeGettersAndSetters(entry.members), isDeprecated); + return getCodeTocData( + filterMergedNamespaceMembers(mergeGettersAndSetters(entry.members)), + isDeprecated, + ); } if (isFunctionEntry(entry)) { diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.mts index 0e48fe1f27e..c8553c2f455 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/decorator-transforms.mts @@ -16,7 +16,6 @@ import { addHtmlUsageNotes, setEntryFlags, } from './jsdoc-transforms.mjs'; -import {addRenderableMembers} from './member-transforms.mjs'; import {addModuleName} from './module-name.mjs'; import {addRepo} from './repo.mjs'; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/enum-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/enum-transforms.mts index 4294b1c7167..ac496c4bc83 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/enum-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/enum-transforms.mts @@ -28,7 +28,7 @@ export async function getEnumRenderable( ): Promise { return setEntryFlags( await addRenderableCodeToc( - addRenderableMembers( + await addRenderableMembers( addHtmlAdditionalLinks( addHtmlUsageNotes( addHtmlJsDocTagComments( @@ -36,6 +36,7 @@ export async function getEnumRenderable( ), ), ), + classEntry.name, ), ), ); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/interface-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/interface-transforms.mts index f1d516e2f34..17f03b8c77c 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/interface-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/interface-transforms.mts @@ -28,7 +28,7 @@ export async function getInterfaceRenderable( ): Promise { return setEntryFlags( await addRenderableCodeToc( - addRenderableMembers( + await addRenderableMembers( addHtmlAdditionalLinks( addHtmlUsageNotes( addHtmlJsDocTagComments( @@ -36,6 +36,7 @@ export async function getInterfaceRenderable( ), ), ), + entry.name, ), ), ); diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts index 4da5fdca1f4..9f88107d792 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/jsdoc-transforms.mts @@ -15,24 +15,24 @@ import { HasDeprecatedFlag, HasDescription, HasDeveloperPreviewFlag, + hasExperimentalFlag, HasHtmlDescription, HasHtmlUsageNotes, HasJsDocTags, HasModuleName, HasRenderableJsDocTags, - hasExperimentalFlag, HasStableFlag, } from '../entities/traits.mjs'; -import {addApiLinksToHtml} from './code-transforms.mjs'; +import {parseMarkdown} from '../../../shared/marked/parse.mjs'; +import {getHighlighterInstance} from '../shiki/shiki.mjs'; import { getCurrentSymbol, getSymbols, getSymbolUrl, unknownSymbolMessage, } from '../symbol-context.mjs'; -import {parseMarkdown} from '../../../shared/marked/parse.mjs'; -import {getHighlighterInstance} from '../shiki/shiki.mjs'; +import {addApiLinksToHtml} from './code-transforms.mjs'; const JS_DOC_USAGE_NOTE_TAGS: Set = new Set(['remarks', 'usageNotes', 'example']); export const JS_DOC_SEE_TAG = 'see'; diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/member-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/member-transforms.mts index 668fe6f5bda..41912dddfc7 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/member-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/member-transforms.mts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import {MemberEntry, MemberTags, MemberType} from '../entities.mjs'; +import {MemberEntry, MemberTags, MemberType, type DocEntry} from '../entities.mjs'; import {isHiddenEntry} from '../entities/categorization.mjs'; +import type {MemberEntryRenderable} from '../entities/renderables.mjs'; import {HasMembers, HasModuleName, HasRenderableMembers, HasRepo} from '../entities/traits.mjs'; +import {addRenderableCodeToc} from './code-transforms.mjs'; import { addHtmlDescription, @@ -39,6 +41,13 @@ export function filterLifecycleMethods(members: MemberEntry[]): MemberEntry[] { return members.filter((m) => !lifecycleMethods.includes(m.name)); } +/** Filters out members that come from merged namespace declarations. */ +export function filterMergedNamespaceMembers(members: MemberEntry[]): MemberEntry[] { + return members.filter( + (m) => m.memberType !== MemberType.Interface && m.memberType !== MemberType.TypeAlias, + ); +} + /** Merges getter and setter entries with the same name into a single entry. */ export function mergeGettersAndSetters(members: MemberEntry[]): MemberEntry[] { const getters = new Set(); @@ -64,20 +73,39 @@ export function mergeGettersAndSetters(members: MemberEntry[]): MemberEntry[] { ); } -export function addRenderableMembers( +function addDisplayName(entry: T, parentName: string): T { + return entry.memberType === MemberType.Interface || entry.memberType === MemberType.TypeAlias + ? {...entry, displayName: `${parentName}.${entry.name}`} + : entry; +} + +export async function addRenderableMembers( entry: T, -): T & HasRenderableMembers { - const members = entry.members - .filter((member) => !isHiddenEntry(member)) - .map((member) => + parentName: string, +): Promise { + const members = ( + await Promise.all( + // TODO: remove `?? []` when components repo is updated to include members array on type aliases. + (entry.members ?? []) + .filter((member) => !isHiddenEntry(member)) + .map((member) => { + if (member.memberType === MemberType.Interface) { + return addRenderableCodeToc( + addModuleName(member as DocEntry & MemberEntry, entry.moduleName), + ); + } else { + return addModuleName(member, entry.moduleName); + } + }), + ) + ).map((member) => + addDisplayName( setEntryFlags( - addHtmlDescription( - addHtmlUsageNotes( - addHtmlJsDocTagComments(addRepo(addModuleName(member, entry.moduleName), entry.repo)), - ), - ), + addHtmlDescription(addHtmlUsageNotes(addHtmlJsDocTagComments(addRepo(member, entry.repo)))), ), - ); + parentName, + ), + ); return { ...entry, diff --git a/adev/shared-docs/pipeline/api-gen/rendering/transforms/type-alias-transforms.mts b/adev/shared-docs/pipeline/api-gen/rendering/transforms/type-alias-transforms.mts index 8668be54a3c..7e58f0cc2e9 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/transforms/type-alias-transforms.mts +++ b/adev/shared-docs/pipeline/api-gen/rendering/transforms/type-alias-transforms.mts @@ -8,7 +8,6 @@ import {TypeAliasEntry} from '../entities.mjs'; import {TypeAliasEntryRenderable} from '../entities/renderables.mjs'; -import type {HasMembers} from '../entities/traits.mjs'; import {addRenderableCodeToc} from './code-transforms.mjs'; import { addHtmlAdditionalLinks, @@ -27,22 +26,20 @@ export async function getTypeAliasRenderable( moduleName: string, repo: string, ): Promise { - const entryWithModuleAndRepo = addRepo(addModuleName(typeAliasEntry, moduleName), repo); - - const renderableMembers = hasMembers(entryWithModuleAndRepo) - ? (await addRenderableMembers(entryWithModuleAndRepo)).members - : undefined; - return setEntryFlags( - await addRenderableCodeToc({ - ...addHtmlAdditionalLinks( - addHtmlUsageNotes(addHtmlJsDocTagComments(addHtmlDescription(entryWithModuleAndRepo))), + await addRenderableCodeToc( + addHtmlAdditionalLinks( + addHtmlUsageNotes( + addHtmlJsDocTagComments( + addHtmlDescription( + await addRenderableMembers( + addRepo(addModuleName(typeAliasEntry, moduleName), repo), + typeAliasEntry.name, + ), + ), + ), + ), ), - members: renderableMembers, - }), + ), ); } - -function hasMembers(typeAlias: TypeAliasEntry): typeAlias is TypeAliasEntry & HasMembers { - return typeAlias.members !== undefined; -} diff --git a/adev/shared-docs/styles/docs/_code.scss b/adev/shared-docs/styles/docs/_code.scss index f355c9e279a..80db31dfdf4 100644 --- a/adev/shared-docs/styles/docs/_code.scss +++ b/adev/shared-docs/styles/docs/_code.scss @@ -161,6 +161,11 @@ $code-font-size: 0.875rem; max-height: 300px; } } + + &.embedded { + border-width: 0; + border-radius: 0; + } } .shiki { @@ -334,7 +339,7 @@ $code-font-size: 0.875rem; text-align: right; padding-block: 0.25rem; } - + .line { &.highlighted { background-color: color-mix(in srgb, var(--bright-blue) 9%, var(--page-background)); diff --git a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts index 56f1b39b1c5..a2b40bb5e4b 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts @@ -47,6 +47,8 @@ export enum MemberType { Getter = 'getter', Setter = 'setter', EnumItem = 'enum_item', + Interface = 'interface', + TypeAlias = 'type_alias', } export enum DecoratorType { @@ -107,7 +109,7 @@ export interface ConstantEntry extends DocEntry { /** Documentation entity for a type alias. */ export interface TypeAliasEntry extends ConstantEntry { generics: GenericEntry[]; - members?: DocEntry[]; // For merged namespaces + members: MemberEntry[]; // For merged namespaces } /** Documentation entity for a TypeScript class. */ @@ -194,6 +196,12 @@ export interface PropertyEntry extends MemberEntry { /** Sub-entry for a class method. */ export type MethodEntry = MemberEntry & FunctionEntry; +/** Sub-entry for an interface (included via namespace). */ +export type InterfaceMemberEntry = MemberEntry & InterfaceEntry; + +/** Sub-entry for a type alias (included via namespace). */ +export type TypeAliasMemberEntry = MemberEntry & TypeAliasEntry; + /** Sub-entry for a single function parameter. */ export interface ParameterEntry { name: string; diff --git a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts index 06491cdd139..23e5fa8bb9f 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts @@ -22,6 +22,9 @@ import { DocEntry, DocEntryWithSourceInfo, EntryType, + MemberType, + type InterfaceEntry, + type MemberEntry, type NamespaceEntry, type TypeAliasEntry, } from './entities'; @@ -150,16 +153,31 @@ export class DocsExtractor { // For now, we only support merging a type alias and a namespace. const typeAlias = entries.find(isTypeAliasEntry); const namespace = entries.find(isNamespaceEntry); + const interfaceEntry = entries.find(isInterfaceEntry); if (typeAlias && namespace) { - typeAlias.members = namespace.members; + typeAlias.members.push(...this.getTypeMembersFromNamespace(namespace)); return typeAlias; } + if (interfaceEntry && namespace) { + interfaceEntry.members.push(...this.getTypeMembersFromNamespace(namespace)); + } } return entry ?? null; } + private getTypeMembersFromNamespace(namespace: NamespaceEntry): MemberEntry[] { + return namespace.members + .filter((e) => isInterfaceEntry(e) || isTypeAliasEntry(e)) + .map((e) => ({ + ...e, + memberType: + e.entryType === EntryType.Interface ? MemberType.Interface : MemberType.TypeAlias, + memberTags: [], + })); + } + /** Extract the doc entry for a single declaration. */ private extractDeclaration(node: ts.Declaration): DocEntry | null { // Ignore anonymous classes. @@ -277,3 +295,7 @@ function isTypeAliasEntry(e: DocEntry | null): e is TypeAliasEntry { function isNamespaceEntry(e: DocEntry | null): e is NamespaceEntry { return e?.entryType === EntryType.Namespace; } + +function isInterfaceEntry(e: DocEntry | null): e is InterfaceEntry { + return e?.entryType === EntryType.Interface; +} diff --git a/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts index c8840f49d40..0e665b8eee1 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/type_alias_extractor.ts @@ -8,8 +8,8 @@ import ts from 'typescript'; import {EntryType} from './entities'; -import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; import {extractGenerics} from './generics_extractor'; +import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; /** Extract the documentation entry for a type alias. */ export function extractTypeAlias(declaration: ts.TypeAliasDeclaration) { @@ -25,5 +25,6 @@ export function extractTypeAlias(declaration: ts.TypeAliasDeclaration) { rawComment: extractRawJsDoc(declaration), description: extractJsDocDescription(declaration), jsdocTags: extractJsDocTags(declaration), + members: [], }; }