Skip to content

Commit 9eb1e05

Browse files
committed
Produce warnings if links cannot be resolved
Ref: #2808
1 parent e4f991d commit 9eb1e05

27 files changed

+313
-31
lines changed

.config/typedoc.json

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
],
1616
"name": "TypeDoc API",
1717

18+
// Don't document the debug entry point
19+
"entryPoints": ["../src/index.ts"],
1820
"outputs": [
1921
{
2022
"name": "html",

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ title: Changelog
66

77
### Bug Fixes
88

9+
- Possibly Breaking: TypeDoc will no longer render anchors within the page for
10+
deeply nested properties. This only affects links to properties of
11+
properties of types, which did not have a clickable link exposed so are
12+
unlikely to have been linked to. Furthermore, these links were not always
13+
created by TypeDoc, only being created if all parent properties contained
14+
comments, #2808.
15+
- TypeDoc will now warn if a property which does not have a URL within the
16+
rendered document and the parent property/page will be linked to instead,
17+
#2808. These warnings can be disabled with the `validation.rewrittenLink`
18+
option.
919
- Fix restoration of groups/categories including documents, #2801.
1020
- Fixed missed relative paths within markdown link references in documents.
1121
- Improved handling of incomplete inline code blocks within markdown.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"exports": {
88
".": "./dist/index.js",
99
"./tsdoc.json": "./tsdoc.json",
10-
"./package.json": "./package.json"
10+
"./package.json": "./package.json",
11+
"./debug": "./dist/lib/debug/index.js"
1112
},
1213
"types": "./dist/index.d.ts",
1314
"bin": {

scripts/capture_screenshots.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export async function captureScreenshots(
9393
headless,
9494
theme,
9595
) {
96+
await fs.promises.rm(outputDirectory, { force: true, recursive: true });
97+
await fs.promises.mkdir(outputDirectory, { recursive: true });
98+
9699
const browser = await puppeteer.launch({
97100
args:
98101
platform() === "win32"

site/development/plugins.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ to. Plugins should assume that they may be loaded multiple times for different
1313
applications, and that a single load of an application class may be used to
1414
convert multiple projects.
1515

16-
Plugins may be either ESM or CommonJS.
16+
Plugins may be either ESM or CommonJS, but TypeDoc ships with ESM, so they should
17+
generally published as ESM to avoid `require(esm)` experimental warnings.
1718

1819
```js
1920
// @ts-check

site/options/validation.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,32 @@ typedoc.json (defaults):
2020
"validation": {
2121
"notExported": true,
2222
"invalidLink": true,
23+
"rewrittenLink": true,
2324
"notDocumented": false,
2425
"unusedMergeModuleWith": true
2526
}
2627
}
2728
```
2829

29-
Specifies validation steps TypeDoc should perform on your generated documentation.
30+
Specifies validation steps TypeDoc should perform on your generated
31+
documentation. Most validation occurs before rendering, but `rewrittenLink` is
32+
done during HTML rendering as links have not been generated before rendering
33+
begins.
34+
35+
- **notExported** - Produce warnings if a type is referenced by the
36+
documentation but the type isn't exported and therefore included in the
37+
documentation.
38+
- **invalidLink** - Produce warnings for `@link` tags which cannot be resolved.
39+
- **rewrittenLink** - Produce warnings for `@link` tags which are resolved,
40+
but whose target does not have a unique URL in the documentation. TypeDoc
41+
will rewrite these links to point to the first parent with a URL.
42+
- **notDocumented** - Produce warnings for reflections which do not have a
43+
documentation comment. This is also controlled by the
44+
[requiredToBeDocumented](#requiredtobedocumented) option.
45+
- **unusedMergeModuleWith** - Produce warnings for
46+
[`@mergeModuleWith`](../tags/mergeModuleWith.md) tags which are not
47+
resolved. This option should generally be disabled if generating JSON which
48+
will be combined with another document later.
3049

3150
## treatWarningsAsErrors
3251

src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
/**
22
* @module TypeDoc API
3+
*
4+
* In addition to the members documented here, TypeDoc exports a `typedoc/debug`
5+
* entry point which exports some functions which may be useful during plugin
6+
* development or debugging. Exports from that entry point are **not stable**
7+
* and may change or be removed at any time.
38
*/
49
export { Application, type ApplicationEvents } from "./lib/application.js";
510

src/lib/converter/comments/declarationReferenceResolver.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ok } from "assert";
22
import {
33
ContainerReflection,
44
DeclarationReflection,
5+
type DocumentReflection,
56
type ProjectReflection,
67
ReferenceReflection,
78
type Reflection,
@@ -225,10 +226,18 @@ function resolveSymbolReferencePart(
225226
let high: Reflection[] = [];
226227
let low: Reflection[] = [];
227228

228-
if (
229-
!(refl instanceof ContainerReflection) ||
230-
!refl.childrenIncludingDocuments
231-
) {
229+
let children:
230+
| ReadonlyArray<DocumentReflection | DeclarationReflection>
231+
| undefined;
232+
233+
if (refl instanceof ContainerReflection) {
234+
children = refl.childrenIncludingDocuments;
235+
}
236+
if (!children && refl.isDeclaration() && refl.type?.type === "reflection") {
237+
children = refl.type.declaration.childrenIncludingDocuments;
238+
}
239+
240+
if (!children) {
232241
return { high, low };
233242
}
234243

@@ -238,12 +247,12 @@ function resolveSymbolReferencePart(
238247
// so that resolution doesn't behave very poorly with projects using JSDoc style resolution.
239248
// Also is more consistent with how TypeScript resolves link tags.
240249
case ".":
241-
high = refl.childrenIncludingDocuments.filter(
250+
high = children.filter(
242251
(r) =>
243252
r.name === path.path &&
244253
(r.kindOf(ReflectionKind.SomeExport) || r.flags.isStatic),
245254
);
246-
low = refl.childrenIncludingDocuments.filter(
255+
low = children.filter(
247256
(r) =>
248257
r.name === path.path &&
249258
(!r.kindOf(ReflectionKind.SomeExport) || !r.flags.isStatic),
@@ -254,7 +263,7 @@ function resolveSymbolReferencePart(
254263
// enum members, type literal properties
255264
case "#":
256265
high =
257-
refl.children?.filter((r) => {
266+
children?.filter((r) => {
258267
return (
259268
r.name === path.path &&
260269
r.kindOf(ReflectionKind.SomeMember) &&
@@ -269,7 +278,7 @@ function resolveSymbolReferencePart(
269278
if (
270279
refl.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)
271280
) {
272-
high = refl.children?.filter((r) => r.name === path.path) || [];
281+
high = children?.filter((r) => r.name === path.path) || [];
273282
}
274283
break;
275284
}

src/lib/converter/comments/linkResolver.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DeclarationReflection,
66
type InlineTagDisplayPart,
77
Reflection,
8+
ReflectionKind,
89
ReflectionSymbolId,
910
} from "../../models/index.js";
1011
import {
@@ -131,12 +132,20 @@ function resolveLinkTag(
131132

132133
// Might already know where it should go if useTsLinkResolution is turned on
133134
if (part.target instanceof ReflectionSymbolId) {
134-
const tsTarget = reflection.project.getReflectionFromSymbolId(
135+
const tsTargets = reflection.project.getReflectionsFromSymbolId(
135136
part.target,
136137
);
137138

138-
if (tsTarget) {
139-
target = tsTarget;
139+
if (tsTargets.length) {
140+
// Find the target most likely to have a real url in the generated documentation
141+
target =
142+
tsTargets.find((r) => r.kindOf(ReflectionKind.SomeExport)) ||
143+
tsTargets.find(
144+
(r) =>
145+
r.kindOf(ReflectionKind.SomeMember) &&
146+
r.parent?.kindOf(ReflectionKind.SomeExport),
147+
) ||
148+
tsTargets[0];
140149
pos = end;
141150
defaultDisplayText =
142151
part.tsLinkText ||
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint-disable no-console */
2+
import type { Application } from "../application.js";
3+
import { ConverterEvents } from "../converter/converter-events.js";
4+
import type { Reflection } from "../models/index.js";
5+
6+
export function debugReflectionLifetimes(app: Application) {
7+
app.converter.on(ConverterEvents.CREATE_PROJECT, logCreate);
8+
app.converter.on(ConverterEvents.CREATE_SIGNATURE, logCreate);
9+
app.converter.on(ConverterEvents.CREATE_TYPE_PARAMETER, logCreate);
10+
app.converter.on(ConverterEvents.CREATE_DECLARATION, logCreate);
11+
app.converter.on(ConverterEvents.CREATE_DOCUMENT, logCreate);
12+
app.converter.on(ConverterEvents.CREATE_PARAMETER, logCreate);
13+
14+
app.converter.on(ConverterEvents.CREATE_PROJECT, (_context, project) => {
15+
const oldRemove = project["_removeReflection"];
16+
project["_removeReflection"] = function (reflection) {
17+
console.log("Remove", reflection.id, reflection.getFullName());
18+
return oldRemove.call(this, reflection);
19+
};
20+
});
21+
}
22+
23+
function logCreate(_context: unknown, refl: Reflection) {
24+
console.log("Create", refl.variant, refl.id, refl.getFullName());
25+
}

src/lib/debug/debugRendererUrls.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* eslint-disable no-console */
2+
import { join } from "node:path";
3+
import type { Application } from "../application.js";
4+
import { Reflection, type SomeReflection } from "../models/index.js";
5+
import type { SerializerComponent } from "../serialization/components.js";
6+
import type { JSONOutput } from "../serialization/index.js";
7+
8+
const serializer: SerializerComponent<SomeReflection> = {
9+
priority: 0,
10+
supports(x) {
11+
return x instanceof Reflection;
12+
},
13+
toObject(item, obj: any) {
14+
obj.url = item.url;
15+
obj.hasOwnDocument = item.hasOwnDocument;
16+
// obj.anchor = item.anchor;
17+
delete obj.sources;
18+
delete obj.groups;
19+
delete obj.categories;
20+
delete obj.readme;
21+
delete obj.content;
22+
delete obj.kind;
23+
delete obj.flags;
24+
delete obj.defaultValue;
25+
delete obj.symbolIdMap;
26+
delete obj.files;
27+
delete obj.packageName;
28+
delete obj.variant;
29+
delete obj.extendedTypes;
30+
delete obj.inheritedFrom;
31+
if (!["reflection", "reference"].includes(obj.type?.type)) {
32+
delete obj.type;
33+
}
34+
35+
if (obj.comment) {
36+
obj.comment.summary = obj.comment.summary.filter(
37+
(part: JSONOutput.CommentDisplayPart) =>
38+
part.kind === "inline-tag",
39+
);
40+
obj.comment.blockTags = obj.comment.blockTags?.filter(
41+
(tag: JSONOutput.CommentTag) => {
42+
tag.content = tag.content.filter(
43+
(part) => part.kind === "inline-tag",
44+
);
45+
return tag.content.length;
46+
},
47+
);
48+
49+
if (
50+
!obj.comment.summary.length &&
51+
!obj.comment.blockTags?.length &&
52+
!obj.comment.modifierTags
53+
) {
54+
delete obj.comment;
55+
}
56+
}
57+
58+
return obj;
59+
},
60+
};
61+
62+
export function debugRendererUrls(
63+
app: Application,
64+
{ json = false, logs = false } = { logs: true },
65+
) {
66+
app.renderer.postRenderAsyncJobs.push(async (evt) => {
67+
if (json) {
68+
app.serializer.addSerializer(serializer);
69+
await app.generateJson(
70+
evt.project,
71+
join(evt.outputDirectory, "url_debug.json"),
72+
);
73+
app.serializer.removeSerializer(serializer);
74+
}
75+
76+
if (logs) {
77+
for (const id in evt.project.reflections) {
78+
const refl = evt.project.reflections[id];
79+
console.log(
80+
refl.id,
81+
refl.getFullName(),
82+
refl.url,
83+
refl.hasOwnDocument,
84+
);
85+
}
86+
}
87+
});
88+
}

src/lib/debug/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { debugRendererUrls } from "./debugRendererUrls.js";
2+
export { debugReflectionLifetimes } from "./debugReflectionLifetimes.js";

src/lib/internationalization/internationalization.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ export class Internationalization {
108108
* Get the translation of the specified key, replacing placeholders
109109
* with the arguments specified.
110110
*/
111-
translate<T extends keyof typeof translatable>(
111+
translate<T extends keyof TranslatableStrings>(
112112
key: T,
113113
...args: TranslatableStrings[T]
114114
): TranslatedString {
115115
return (
116116
this.allTranslations.get(this.application?.lang ?? "en").get(key) ??
117-
translatable[key]
117+
translatable[key as keyof typeof translatable]
118118
).replace(/\{(\d+)\}/g, (_, index) => {
119119
return args[+index] ?? "(no placeholder)";
120120
}) as TranslatedString;

src/lib/internationalization/locales/en.cts

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export = {
9595
"The following symbols were marked as intentionally not exported, but were either not referenced in the documentation, or were exported:\n\t{0}",
9696
reflection_0_has_unused_mergeModuleWith_tag:
9797
"{0} has a @mergeModuleWith tag which could not be resolved",
98+
reflection_0_links_to_1_with_text_2_but_resolved_to_3: `"{0}" links to "{1}" with text "{2}" which exists but does not have a link in the documentation, will link to "{3}" instead.`,
9899

99100
// conversion plugins
100101
not_all_search_category_boosts_used_0: `Not all categories specified in searchCategoryBoosts were used in the documentation. The unused categories were:\n\t{0}`,

src/lib/models/reflections/project.ts

+1
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ export class ProjectReflection extends ContainerReflection {
391391

392392
/** @internal */
393393
registerSymbolId(reflection: Reflection, id: ReflectionSymbolId) {
394+
this.removedSymbolIds.delete(id);
394395
this.reflectionIdToSymbolIdMap.set(reflection.id, id);
395396

396397
const previous = this.symbolToReflectionIdMap.get(id);

src/lib/models/types.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,13 @@ export class ReferenceType extends Type {
833833
const resolved =
834834
resolvePotential.find((refl) => refl.kindOf(kind)) ||
835835
resolvePotential.find((refl) => refl.kindOf(~kind))!;
836-
this._target = resolved.id;
836+
837+
// Do not mark the type as resolved at this point so that if it
838+
// points to a member which is removed (e.g. by typedoc-plugin-zod)
839+
// and then replaced it still ends up pointing at the right reflection.
840+
// We will lock type reference resolution when serializing to JSON.
841+
// this._target = resolved.id;
842+
837843
return resolved;
838844
}
839845

@@ -1002,6 +1008,8 @@ export class ReferenceType extends Type {
10021008
target = this._target;
10031009
} else if (this._project?.symbolIdHasBeenRemoved(this._target)) {
10041010
target = -1;
1011+
} else if (this.reflection) {
1012+
target = this.reflection.id;
10051013
} else {
10061014
target = this._target.toObject(serializer);
10071015
}
@@ -1023,7 +1031,7 @@ export class ReferenceType extends Type {
10231031
result.refersToTypeParameter = true;
10241032
}
10251033

1026-
if (typeof this._target !== "number" && this.preferValues) {
1034+
if (typeof target !== "number" && this.preferValues) {
10271035
result.preferValues = true;
10281036
}
10291037

0 commit comments

Comments
 (0)