Skip to content

Commit 37501a2

Browse files
committed
Support for comments on type alias union members
Fixes #2585
1 parent afd8de5 commit 37501a2

File tree

21 files changed

+315
-113
lines changed

21 files changed

+315
-113
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
are currently shipped in the package, but it is now possible to add support for additional languages, #2475.
4444
- Added support for a `packageOptions` object which specifies options that should be applied to each entry point when running with `--entryPointStrategy packages`, #2523.
4545
- `--hostedBaseUrl` will now be used to generate a `<link rel="canonical">` element in the project root page, #2550.
46+
- Added support for documenting individual elements of a union type, #2585.
47+
Note: This feature is only available on type aliases directly containing unions.
4648
- New option, `--customFooterHtml` to add custom HTML to the generated page footer, #2559.
4749
- TypeDoc will now copy modifier tags to children if specified in the `--cascadedModifierTags` option, #2056.
4850
- TypeDoc will now warn if mutually exclusive modifier tags are specified for a comment (e.g. both `@alpha` and `@beta`), #2056.
@@ -54,6 +56,8 @@
5456
If `--projectDocuments` is used to add documents, this option defaults to `true`, otherwise, defaults to `false`.
5557
- Added new `--highlightLanguages` option to control what Shiki language packages are loaded.
5658
- TypeDoc will now render union elements on new lines if there are more than 3 items in the union.
59+
- TypeDoc will now only render the "Type Declaration" section if it will provide additional information not already presented in the page.
60+
This results in significantly smaller documentation pages in many cases where that section would just repeat what has already been presented in the rendered type.
5761

5862
### Bug Fixes
5963

@@ -68,6 +72,7 @@
6872
- Fixed issue where search results could not be navigated while Windows Narrator was on, #2563.
6973
- `charset` is now correctly cased in `<meta>` tag generated by the default theme, #2568.
7074
- Fixed very slow conversion on Windows where Msys git was used by typedoc to discover repository links, #2586.
75+
- The `--hideParameterTypesInTitle` option no longer applies when rendering function types.
7176
- Fixed `externalSymbolLinkMappings` option's support for [meanings](https://typedoc.org/guides/declaration-references/#meaning) in declaration references.
7277
- Buttons to copy code now have the `type=button` attribute set to avoid being treated as submit buttons.
7378
- `--hostedBaseUrl` will now implicitly add a trailing slash to the generated URL.

scripts/capture_screenshots.mjs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ function findHtmlPages(root) {
8484
* @param {string} outputDirectory
8585
* @param {number} jobs
8686
* @param {boolean} headless
87+
* @param {string} theme
8788
*/
8889
export async function captureScreenshots(
8990
baseDirectory,
9091
outputDirectory,
9192
jobs,
9293
headless,
94+
theme,
9395
) {
9496
const browser = await puppeteer.launch({
9597
args:
@@ -104,10 +106,9 @@ export async function captureScreenshots(
104106
console.log(`Processing ${pages.length} pages with ${jobs} workers`);
105107
for (const file of pages) {
106108
queue.add(async () => {
107-
console.log("Starting", file);
108109
const outputPath = resolve(
109110
outputDirectory,
110-
relative(baseDirectory, file).replace(/\.html$/, ""),
111+
relative(baseDirectory, file).replace(/\.html$/, ".png"),
111112
);
112113
fs.mkdirSync(dirname(outputPath), { recursive: true });
113114

@@ -118,26 +119,16 @@ export async function captureScreenshots(
118119
waitUntil: ["domcontentloaded"],
119120
});
120121

121-
await page.evaluate(() => {
122-
document.documentElement.dataset.theme = "light";
123-
});
124-
await page.screenshot({
125-
path: outputPath + "-light.png",
126-
fullPage: true,
127-
});
128-
console.log("Captured light image for", file);
129-
130-
await page.evaluate(() => {
131-
document.documentElement.dataset.theme = "dark";
132-
});
122+
await page.evaluate(
123+
`document.documentElement.dataset.theme = "${theme}"`,
124+
);
133125
await page.screenshot({
134-
path: outputPath + "-dark.png",
126+
path: outputPath,
135127
fullPage: true,
136128
});
137-
console.log("Captured dark image for", file);
138129

139130
await context.close();
140-
console.log("Finished", file);
131+
console.log("Finished", relative(baseDirectory, file));
141132
});
142133
}
143134

@@ -169,6 +160,10 @@ if (import.meta.url.endsWith(process.argv[1])) {
169160
type: "boolean",
170161
default: false,
171162
},
163+
theme: {
164+
type: "string",
165+
default: "light",
166+
},
172167
},
173168
});
174169

@@ -178,6 +173,12 @@ if (import.meta.url.endsWith(process.argv[1])) {
178173

179174
const start = Date.now();
180175
await fs.promises.rm(output, { recursive: true, force: true });
181-
await captureScreenshots(docs, output, jobs, !args.values.debug);
176+
await captureScreenshots(
177+
docs,
178+
output,
179+
jobs,
180+
!args.values.debug,
181+
args.values.theme || "light",
182+
);
182183
console.log(`Took ${(Date.now() - start) / 1000} seconds`);
183184
}

src/lib/converter/comments/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export function getComment(
196196

197197
export function getNodeComment(
198198
node: ts.Node,
199-
kind: ReflectionKind,
199+
moduleComment: boolean,
200200
config: CommentParserConfig,
201201
logger: Logger,
202202
commentStyle: CommentStyle,
@@ -207,7 +207,7 @@ export function getNodeComment(
207207
discoverNodeComment(node, commentStyle),
208208
config,
209209
logger,
210-
kind === ReflectionKind.Module,
210+
moduleComment,
211211
checker,
212212
files,
213213
);

src/lib/converter/context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,10 @@ export class Context {
294294
);
295295
}
296296

297-
getNodeComment(node: ts.Node, kind: ReflectionKind) {
297+
getNodeComment(node: ts.Node, moduleComment: boolean) {
298298
return getNodeComment(
299299
node,
300-
kind,
300+
moduleComment,
301301
this.converter.config,
302302
this.logger,
303303
this.converter.commentStyle,

src/lib/converter/factories/index-signature.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function convertIndexSignatures(context: Context, symbol: ts.Symbol) {
2626
ReflectionKind.IndexSignature,
2727
context.scope,
2828
);
29-
index.comment = context.getNodeComment(indexDeclaration, index.kind);
29+
index.comment = context.getNodeComment(indexDeclaration, false);
3030
index.parameters = [
3131
new ParameterReflection(
3232
param.name.getText(),

src/lib/converter/symbols.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type Reflection,
99
ReflectionFlag,
1010
ReflectionKind,
11+
UnionType,
1112
} from "../models";
1213
import {
1314
getEnumFlags,
@@ -349,6 +350,10 @@ function convertTypeAlias(
349350
declaration.type,
350351
);
351352

353+
if (reflection.type.type === "union") {
354+
attachUnionComments(context, declaration, reflection.type);
355+
}
356+
352357
context.finalizeDeclarationReflection(reflection);
353358

354359
// Do this after finalization so that the CommentPlugin can get @typeParam tags
@@ -366,6 +371,40 @@ function convertTypeAlias(
366371
}
367372
}
368373

374+
function attachUnionComments(
375+
context: Context,
376+
declaration: ts.TypeAliasDeclaration,
377+
union: UnionType,
378+
) {
379+
const list = declaration.type.getChildAt(0);
380+
if (list.kind !== ts.SyntaxKind.SyntaxList) return;
381+
382+
let unionIndex = 0;
383+
for (const child of list.getChildren()) {
384+
const comment = context.getNodeComment(child, false);
385+
if (comment?.modifierTags.size || comment?.blockTags.length) {
386+
context.logger.warn(
387+
context.logger.i18n.comment_for_0_should_not_contain_block_or_modifier_tags(
388+
context.scope.getFriendlyFullName() + "." + unionIndex,
389+
),
390+
child,
391+
);
392+
}
393+
394+
if (comment) {
395+
union.elementSummaries ||= Array.from(
396+
{ length: union.types.length },
397+
() => [],
398+
);
399+
union.elementSummaries[unionIndex] = comment.summary;
400+
}
401+
402+
if (child.kind !== ts.SyntaxKind.BarToken) {
403+
++unionIndex;
404+
}
405+
}
406+
}
407+
369408
function convertTypeAliasAsInterface(
370409
context: Context,
371410
symbol: ts.Symbol,

src/lib/converter/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,7 @@ const unionConverter: TypeConverter<ts.UnionTypeNode, ts.UnionType> = {
10671067
},
10681068
convertType(context, type) {
10691069
const types = type.types.map((type) => convertType(context, type));
1070+
normalizeUnion(types);
10701071
sortLiteralUnion(types);
10711072

10721073
return new UnionType(types);
@@ -1152,3 +1153,32 @@ function sortLiteralUnion(types: SomeType[]) {
11521153
return (aLit.value as number) - (bLit.value as number);
11531154
});
11541155
}
1156+
1157+
function normalizeUnion(types: SomeType[]) {
1158+
let trueIndex = -1;
1159+
let falseIndex = -1;
1160+
for (
1161+
let i = 0;
1162+
i < types.length && (trueIndex === -1 || falseIndex === -1);
1163+
i++
1164+
) {
1165+
const t = types[i];
1166+
if (t instanceof LiteralType) {
1167+
if (t.value === true) {
1168+
trueIndex = i;
1169+
}
1170+
if (t.value === false) {
1171+
falseIndex = i;
1172+
}
1173+
}
1174+
}
1175+
1176+
if (trueIndex !== -1 && falseIndex !== -1) {
1177+
types.splice(Math.max(trueIndex, falseIndex), 1);
1178+
types.splice(
1179+
Math.min(trueIndex, falseIndex),
1180+
1,
1181+
new IntrinsicType("boolean"),
1182+
);
1183+
}
1184+
}

src/lib/internationalization/translatable.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export const translatable = {
5353
converting_0_as_class_requires_value_declaration: `Converting {0} as a class requires a declaration which represents a non-type value.`,
5454
converting_0_as_class_without_construct_signatures: `{0} is being converted as a class, but does not have any construct signatures`,
5555

56+
comment_for_0_should_not_contain_block_or_modifier_tags: `The comment for {0} should not contain any block or modifier tags.`,
57+
5658
symbol_0_has_multiple_declarations_with_comment: `{0} has multiple declarations with a comment. An arbitrary comment will be used.`,
5759
comments_for_0_are_declared_at_1: `The comments for {0} are declared at:\n\t{1}`,
5860

src/lib/models/comments/comment.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,26 @@ import { NonEnumerable } from "../../utils/general";
88
/**
99
* Represents a parsed piece of a comment.
1010
* @category Comments
11+
* @see {@link JSONOutput.CommentDisplayPart}
1112
*/
1213
export type CommentDisplayPart =
14+
/**
15+
* Represents a plain text portion of the comment, may contain markdown
16+
*/
1317
| { kind: "text"; text: string }
18+
/**
19+
* Represents a code block separated out form the plain text entry so
20+
* that TypeDoc knows to skip it when parsing relative links and inline tags.
21+
**/
1422
| { kind: "code"; text: string }
23+
/**
24+
* Represents an inline tag like `{@link Foo}`
25+
*/
1526
| InlineTagDisplayPart
27+
/**
28+
* Represents a reference to a path relative to where the comment resides.
29+
* This is used to detect and copy relative image links.
30+
*/
1631
| RelativeLinkDisplayPart;
1732

1833
/**

src/lib/models/types.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ReflectionSymbolId } from "./reflections/ReflectionSymbolId";
99
import type { DeclarationReference } from "../converter/comments/declarationReference";
1010
import { findPackageForPath } from "../utils/fs";
1111
import { ReflectionKind } from "./reflections/kind";
12+
import { Comment, CommentDisplayPart } from "./comments";
1213

1314
/**
1415
* Base class of all type definitions.
@@ -1315,9 +1316,16 @@ export class TypeOperatorType extends Type {
13151316
export class UnionType extends Type {
13161317
override readonly type = "union";
13171318

1319+
/**
1320+
* If present, there should be as many items in this array as there are items in the {@link types} array.
1321+
*
1322+
* This member is only valid on unions which are on {@link DeclarationReflection.type | DeclarationReflection.type} with a
1323+
* {@link ReflectionKind} `kind` of `TypeAlias`. Specifying it on any other union is undefined behavior.
1324+
*/
1325+
elementSummaries?: CommentDisplayPart[][];
1326+
13181327
constructor(public types: SomeType[]) {
13191328
super();
1320-
this.normalize();
13211329
}
13221330

13231331
protected override getTypeString(): string {
@@ -1356,31 +1364,10 @@ export class UnionType extends Type {
13561364
return map[context];
13571365
}
13581366

1359-
private normalize() {
1360-
let trueIndex = -1;
1361-
let falseIndex = -1;
1362-
for (
1363-
let i = 0;
1364-
i < this.types.length && (trueIndex === -1 || falseIndex === -1);
1365-
i++
1366-
) {
1367-
const t = this.types[i];
1368-
if (t instanceof LiteralType) {
1369-
if (t.value === true) {
1370-
trueIndex = i;
1371-
}
1372-
if (t.value === false) {
1373-
falseIndex = i;
1374-
}
1375-
}
1376-
}
1377-
1378-
if (trueIndex !== -1 && falseIndex !== -1) {
1379-
this.types.splice(Math.max(trueIndex, falseIndex), 1);
1380-
this.types.splice(
1381-
Math.min(trueIndex, falseIndex),
1382-
1,
1383-
new IntrinsicType("boolean"),
1367+
override fromObject(de: Deserializer, obj: JSONOutput.UnionType): void {
1368+
if (obj.elementSummaries) {
1369+
this.elementSummaries = obj.elementSummaries.map((parts) =>
1370+
Comment.deserializeDisplayParts(de, parts),
13841371
);
13851372
}
13861373
}
@@ -1389,6 +1376,9 @@ export class UnionType extends Type {
13891376
return {
13901377
type: this.type,
13911378
types: this.types.map((t) => serializer.toObject(t)),
1379+
elementSummaries: this.elementSummaries?.map((parts) =>
1380+
Comment.serializeDisplayParts(serializer, parts),
1381+
),
13921382
};
13931383
}
13941384
}

src/lib/output/models/UrlMapping.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ export class UrlMapping<Model = any> {
1919
}
2020
}
2121

22+
/**
23+
* @param data the reflection to render
24+
* @returns either a string to be written to the file, or an element to be serialized and then written.
25+
*/
2226
export type RenderTemplate<T> = (data: T) => JSX.Element | string;

0 commit comments

Comments
 (0)