Skip to content

Commit d27a719

Browse files
committed
Add support for externalSymbolLinkMappings
1 parent 3872463 commit d27a719

File tree

9 files changed

+138
-7
lines changed

9 files changed

+138
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Features
44

5+
- Added support for defining one-off external link mappings with `externalSymbolLinkMappings` see
6+
[the documentation](https://typedoc.org/guides/options/#externalsymbollinkmappings) for usage examples and caveats, #2030.
57
- External link resolvers defined with `addUnknownSymbolResolver` will now be checked when resolving `@link` tags, #2030.
68
Note: To support this, resolution will now happen during conversion, and as such, `Renderer.addUnknownSymbolResolver` has been
79
soft deprecated in favor of `Converter.addUnknownSymbolResolver`. Plugins should update to use the method on `Converter`.

internal-docs/third-party-symbols.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
11
# Third Party Symbols
22

33
TypeDoc 0.22 added support for linking to third party sites by associating a symbol name with npm packages.
4-
Plugins can add support for linking to third party sites by calling `app.renderer.addUnknownSymbolResolver`.
4+
5+
Since TypeDoc 0.23.13, some mappings can be defined without a plugin by setting `externalSymbolLinkMappings`.
6+
This should be set to an object whose keys are package names, and values are the `.` joined qualified name
7+
of the third party symbol. If the link was defined with a user created declaration reference, it may also
8+
have a `:meaning` at the end. TypeDoc will _not_ attempt to perform fuzzy matching to remove the meaning from
9+
keys if not specified, so if meanings may be used, a url must be listed multiple times.
10+
11+
Global external symbols are supported, but may have surprising behavior. TypeDoc assumes that if a symbol was
12+
referenced from a package, it was exported from that package. This will be true for most native TypeScript packages,
13+
but packages which rely on `@types` will be linked according to that `@types` package for that package name.
14+
15+
Furthermore, types which are defined in the TypeScript lib files (including `Array`, `Promise`, ...) will be
16+
detected as belonging to the `typescript` package rather than the `global` package. In order to support both
17+
`{@link !Promise}` and references to the type within source code, both `global` and `typescript` need to be set.
18+
19+
```jsonc
20+
// typedoc.json
21+
{
22+
"externalSymbolLinkMappings": {
23+
"global": {
24+
// Handle {@link !Promise}
25+
"Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"
26+
},
27+
"typescript": {
28+
// Handle type X = Promise<number>
29+
"Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"
30+
}
31+
}
32+
}
33+
```
34+
35+
Plugins can add support for linking to third party sites by calling `app.converter.addUnknownSymbolResolver`.
536

637
If the given symbol is unknown, or does not appear in the documentation site, the resolver may return `undefined`
738
and no link will be rendered unless provided by another resolver.
@@ -19,7 +50,9 @@ const knownSymbols = {
1950
export function load(app: Application) {
2051
app.converter.addUnknownSymbolResolver((ref: DeclarationReference) => {
2152
if (
53+
// TS defined symbols
2254
ref.moduleSource !== "@types/react" &&
55+
// User {@link} tags
2356
ref.moduleSource !== "react"
2457
) {
2558
return;

src/lib/converter/comments/linkResolver.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,20 @@ function resolveLinkTag(
136136
const declRef = parseDeclarationReference(part.text, pos, end);
137137

138138
let target: Reflection | string | undefined;
139+
let defaultDisplayText: string;
139140
if (declRef) {
140141
// Got one, great! Try to resolve the link
141142
target = resolveDeclarationReference(reflection, declRef[0]);
142143
pos = declRef[1];
143144

144-
// If we didn't find a link, it might be a @link tag to an external symbol, check that next.
145-
if (!target) {
145+
if (target) {
146+
defaultDisplayText = target.name;
147+
} else {
148+
// If we didn't find a link, it might be a @link tag to an external symbol, check that next.
146149
target = attemptExternalResolve(declRef[0]);
150+
if (target) {
151+
defaultDisplayText = part.text.substring(0, pos);
152+
}
147153
}
148154
}
149155

@@ -153,6 +159,7 @@ function resolveLinkTag(
153159
target =
154160
wsIndex === -1 ? part.text : part.text.substring(0, wsIndex);
155161
pos = target.length;
162+
defaultDisplayText = target;
156163
}
157164
}
158165

@@ -178,9 +185,7 @@ function resolveLinkTag(
178185
}
179186

180187
part.target = target;
181-
part.text =
182-
part.text.substring(pos).trim() ||
183-
(typeof target === "string" ? target : target.name);
188+
part.text = part.text.substring(pos).trim() || defaultDisplayText!;
184189

185190
return part;
186191
}

src/lib/converter/converter.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export class Converter extends ChildableComponent<
7272
@BindOption("validation")
7373
validation!: ValidationOptions;
7474

75+
/** @internal */
76+
@BindOption("externalSymbolLinkMappings")
77+
externalSymbolLinkMappings!: Record<string, Record<string, string>>;
78+
7579
private _config?: CommentParserConfig;
7680
private _externalSymbolResolvers: Array<
7781
(ref: DeclarationReference) => string | undefined
@@ -159,6 +163,36 @@ export class Converter extends ChildableComponent<
159163
*/
160164
static readonly EVENT_RESOLVE_END = ConverterEvents.RESOLVE_END;
161165

166+
constructor(owner: Application) {
167+
super(owner);
168+
169+
this.addUnknownSymbolResolver((ref) => {
170+
// Require global links, matching local ones will likely hide mistakes where the
171+
// user meant to link to a local type.
172+
if (ref.resolutionStart !== "global" || !ref.symbolReference) {
173+
return;
174+
}
175+
176+
const modLinks =
177+
this.externalSymbolLinkMappings[ref.moduleSource ?? "global"];
178+
if (typeof modLinks !== "object") {
179+
return;
180+
}
181+
182+
let name = "";
183+
if (ref.symbolReference.path) {
184+
name += ref.symbolReference.path.map((p) => p.path).join(".");
185+
}
186+
if (ref.symbolReference.meaning) {
187+
name += ":" + ref.symbolReference.meaning;
188+
}
189+
190+
if (typeof modLinks[name] === "string") {
191+
return modLinks[name];
192+
}
193+
});
194+
}
195+
162196
/**
163197
* Compile the given source files and create a project reflection for them.
164198
*/

src/lib/utils/options/declaration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ export interface TypeDocOptionMap {
8888
excludeInternal: boolean;
8989
excludePrivate: boolean;
9090
excludeProtected: boolean;
91+
externalSymbolLinkMappings: ManuallyValidatedOption<
92+
Record<string, Record<string, string>>
93+
>;
9194
media: string;
9295
includes: string;
9396

src/lib/utils/options/sources/typedoc.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,32 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
9999
help: "Ignore protected variables and methods.",
100100
type: ParameterType.Boolean,
101101
});
102+
options.addDeclaration({
103+
name: "externalSymbolLinkMappings",
104+
help: "Define custom links for symbols not included in the documentation.",
105+
type: ParameterType.Mixed,
106+
defaultValue: {},
107+
validate(value) {
108+
const error =
109+
"externalSymbolLinkMappings must be a Record<package name, Record<symbol name, link>>";
110+
111+
if (!Validation.validate({}, value)) {
112+
throw new Error(error);
113+
}
114+
115+
for (const mappings of Object.values(value)) {
116+
if (!Validation.validate({}, mappings)) {
117+
throw new Error(error);
118+
}
119+
120+
for (const link of Object.values(mappings)) {
121+
if (typeof link !== "string") {
122+
throw new Error(error);
123+
}
124+
}
125+
}
126+
},
127+
});
102128
options.addDeclaration({
103129
name: "media",
104130
help: "Specify the location with media files that should be copied to the output directory.",

src/lib/validation/exports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function validateExports(
7676
for (const { type, owner } of discoverAllReferenceTypes(project, true)) {
7777
// If we don't have a symbol, then this was an intentionally broken reference.
7878
const symbol = type.getSymbol();
79-
if (!type.reflection && symbol) {
79+
if (!type.reflection && !type.externalUrl && symbol) {
8080
if (
8181
(symbol.flags & ts.SymbolFlags.TypeParameter) === 0 &&
8282
!intentional.has(symbol, type.qualifiedName) &&

src/test/behaviorTests.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,29 @@ export const behaviorTests: {
217217
equal(Comment.combineDisplayParts(foo.comment?.summary), "export foo");
218218
},
219219

220+
_externalSymbols(app) {
221+
app.options.setValue("externalSymbolLinkMappings", {
222+
global: {
223+
Promise: "/promise",
224+
},
225+
typescript: {
226+
Promise: "/promise2",
227+
},
228+
});
229+
},
230+
externalSymbols(project) {
231+
const p = query(project, "P");
232+
equal(p.comment?.summary?.[1], {
233+
kind: "inline-tag",
234+
tag: "@link",
235+
target: "/promise",
236+
text: "!Promise",
237+
});
238+
239+
equal(p.type?.type, "reference" as const);
240+
equal(p.type.externalUrl, "/promise2");
241+
},
242+
220243
groupTag(project) {
221244
const A = query(project, "A");
222245
const B = query(project, "B");
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Testing custom external link resolution
3+
* {@link !Promise}
4+
*/
5+
export type P = Promise<string>;

0 commit comments

Comments
 (0)