From 13a847a908f9e115410a822ab6e7fe7b08c2def0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Wed, 17 Jun 2026 16:46:22 +0200 Subject: [PATCH] feat: render `@since` on top-level symbols; refine inherited handling - Show an "Added in" badge on top-level symbols (classes, interfaces, enums, type aliases). Previously `@since` only rendered on members, so a symbol's own version was never displayed. Callable symbols still render it on their signatures, so the badge is suppressed there to avoid duplication. - Render `@since` only when explicitly present on a reflection (no walking up the parent chain). Members inherited from within the documented packages keep the `@since` TypeDoc copies from their base, matching normal doc inheritance. - Drop `@since` for members inherited from native runtime types (the TypeScript standard library and `@types/node`), whose copied tags carry no meaningful version. Tags inherited from other dependencies are kept. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/Comment.tsx | 49 ++++++++++++++++++++++++-- src/components/MemberDeclaration.tsx | 8 ++--- src/components/MemberSignatureBody.tsx | 10 +++--- src/components/Reflection.tsx | 15 +++++++- 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/src/components/Comment.tsx b/src/components/Comment.tsx index 61199d6a..3cb06157 100644 --- a/src/components/Comment.tsx +++ b/src/components/Comment.tsx @@ -20,6 +20,52 @@ export function hasComment(comment?: JSONOutput.Comment): boolean { ); } +interface SinceReflection { + comment?: JSONOutput.Comment; + inheritedFrom?: JSONOutput.ReferenceType; + sources?: JSONOutput.SourceReference[]; +} + +// Native runtime symbols: the TypeScript standard library (`lib.*.d.ts`) and +// Node's built-in type definitions (`@types/node`). These have no meaningful +// `@since` of their own — a `@since` on such a member would be copied noise — so +// we always drop it. Other dependencies under `node_modules` are NOT native: +// their `@since` tags (if any) reflect that package's real release history, so +// inherited members keep them. +const NATIVE_SOURCE = /(?:^|\/)node_modules\/(?:typescript\/lib\/|@types\/node\/)/; + +function isNativeReflection(reflection: SinceReflection): boolean { + const fileName = reflection.sources?.[0]?.fileName; + + return !!fileName && NATIVE_SOURCE.test(fileName); +} + +// Return the content of an `@since` tag explicitly present on the reflection. +// +// TypeDoc copies a base member's doc comment onto the members that inherit it, +// so an inherited symbol naturally carries the `@since` it was given in its +// base — we keep that, mirroring how every other inherited doc behaves +// (including symbols inherited from other packages). The exception is members +// inherited from a native runtime type (e.g. `Error`): those carry no real +// version, so we drop it. +export function getSinceContent( + reflection: SinceReflection | undefined, +): JSONOutput.CommentDisplayPart[] | undefined { + const content = reflection?.comment?.blockTags?.find( + (blockTag) => blockTag.tag === '@since', + )?.content; + + if (!content) { + return undefined; + } + + if (reflection.inheritedFrom && isNativeReflection(reflection)) { + return undefined; + } + + return content; +} + export function displayPartsToMarkdown(parts: JSONOutput.CommentDisplayPart[]): string { return parts .map((part) => { @@ -38,8 +84,7 @@ export function Comment({ comment, root, hideTags = [] }: CommentProps) { } // Hide custom tags. - hideTags.push('@reference'); - hideTags.push('@since'); + hideTags.push('@reference', '@since'); const blockTags = comment.blockTags?.filter((tag) => { diff --git a/src/components/MemberDeclaration.tsx b/src/components/MemberDeclaration.tsx index d07eb3bd..4303c2a9 100644 --- a/src/components/MemberDeclaration.tsx +++ b/src/components/MemberDeclaration.tsx @@ -3,7 +3,7 @@ import { useMinimalLayout } from '../hooks/useMinimalLayout'; import { useRequiredReflection } from '../hooks/useReflection'; import { escapeMdx } from '../utils/helpers'; -import { Comment, displayPartsToMarkdown, hasComment } from './Comment'; +import { Comment, displayPartsToMarkdown, getSinceContent, hasComment } from './Comment'; import { DefaultValue } from './DefaultValue'; import { Icon } from './Icon'; import { Markdown } from './Markdown'; @@ -22,7 +22,7 @@ export function MemberDeclaration({ id }: MemberDeclarationProps) { const minimal = useMinimalLayout(); const showTypes = reflection.typeParameters && reflection.typeParameters.length > 0; const showDeclaration = !minimal && extractDeclarationFromType(reflection.type); - const showSince = reflection.comment?.blockTags?.some((tag) => tag.tag === '@since'); + const sinceContent = getSinceContent(reflection); return ( <> @@ -64,9 +64,9 @@ export function MemberDeclaration({ id }: MemberDeclarationProps) { )} - {showSince && ( + {sinceContent && (
- tag.tag === '@since')?.content)} /> +
)} diff --git a/src/components/MemberSignatureBody.tsx b/src/components/MemberSignatureBody.tsx index 6e89cd40..a41253d0 100644 --- a/src/components/MemberSignatureBody.tsx +++ b/src/components/MemberSignatureBody.tsx @@ -7,15 +7,15 @@ import { usePluginData } from '@docusaurus/useGlobalData'; import { useMinimalLayout } from '../hooks/useMinimalLayout'; import type { TSDSignatureReflection } from '../types'; import { ApiDataContext } from './ApiDataContext'; -import { Comment, displayPartsToMarkdown, hasComment } from './Comment'; +import { Comment, displayPartsToMarkdown, getSinceContent, hasComment } from './Comment'; import { CommentBadges, isCommentWithModifiers } from './CommentBadges'; import { DefaultValue } from './DefaultValue'; import { Flags } from './Flags'; +import { Markdown } from './Markdown'; import { hasSources, MemberSources } from './MemberSources'; import { Parameter } from './Parameter'; import { extractDeclarationFromType, Type } from './Type'; import { TypeParameters } from './TypeParameters'; -import { Markdown } from './Markdown'; export function hasSigBody( sig: TSDSignatureReflection | undefined, @@ -63,9 +63,9 @@ export function MemberSignatureBody({ hideSources, sig }: MemberSignatureBodyPro const showTypes = sig.typeParameter && sig.typeParameter.length > 0; const showParams = !minimal && sig.parameters && sig.parameters.length > 0; const showReturn = !minimal && sig.type; - const showSince = sig.comment?.blockTags?.some((tag) => tag.tag === '@since'); const { reflections } = useContext(ApiDataContext); + const sinceContent = getSinceContent(sig); const { isPython } = usePluginData('docusaurus-plugin-typedoc-api') as GlobalData; if (isPython) { @@ -218,10 +218,10 @@ export function MemberSignatureBody({ hideSources, sig }: MemberSignatureBodyPro )} { - showSince && ( + sinceContent && ( <>
- tag.tag === '@since')?.content)} /> +
) diff --git a/src/components/Reflection.tsx b/src/components/Reflection.tsx index 4150696e..69bfd918 100644 --- a/src/components/Reflection.tsx +++ b/src/components/Reflection.tsx @@ -3,11 +3,12 @@ import { useMemo } from 'react'; import type { TSDDeclarationReflection, TSDReflection, TSDSignatureReflection } from '../types'; import { createHierarchy } from '../utils/hierarchy'; -import { Comment, hasComment } from './Comment'; +import { Comment, displayPartsToMarkdown, getSinceContent, hasComment } from './Comment'; import { CommentBadges, isCommentWithModifiers } from './CommentBadges'; import { Hierarchy } from './Hierarchy'; import { Icon } from './Icon'; import { Index } from './Index'; +import { Markdown } from './Markdown'; import { Members } from './Members'; import { MemberSignatures } from './MemberSignatures'; import { Parameter } from './Parameter'; @@ -20,12 +21,24 @@ export interface ReflectionProps { export function Reflection({ reflection }: ReflectionProps) { const hierarchy = useMemo(() => createHierarchy(reflection), [reflection]); + // Callable top-level symbols (functions) render `@since` on their signatures + // below, so only surface it here for non-callable symbols (classes, + // interfaces, enums, type aliases, ...) where it would otherwise be missing. + const hasOwnSignatures = + 'signatures' in reflection && !!reflection.signatures && reflection.signatures.length > 0; + const sinceContent = hasOwnSignatures ? undefined : getSinceContent(reflection); return ( <> {isCommentWithModifiers(reflection.comment) && } {hasComment(reflection.comment) && } + {sinceContent && ( +
+ +
+ )} + {'typeParameter' in reflection && reflection.typeParameter && reflection.typeParameter.length > 0 &&