Skip to content

Commit c10070d

Browse files
committed
fix(type-gen): improve ownership
1 parent d951b28 commit c10070d

3 files changed

Lines changed: 176 additions & 15 deletions

File tree

plugins/processor/index.mjs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { applySourceMetadata } from './source.mjs';
88
import { DocKitRouter } from './router.mjs';
99
import { sidebar } from './site.mjs';
1010
import { createTypeMap } from './typeMap.mjs';
11-
import { categoryForReflection } from '../shared/categories.mjs';
1211

1312
/**
1413
* @param {import('typedoc-plugin-markdown').MarkdownApplication} app
@@ -71,19 +70,6 @@ export function load(app) {
7170
const internalModule = project.children?.find(c => c.name === '<internal>');
7271

7372
if (internalModule) {
74-
const importantTypes = [];
75-
76-
internalModule.children.forEach(child => {
77-
// TODO: We are calling `categoryForReflection` here, and then again when routing
78-
// We should cache the result to avoid re-looking up that data
79-
if (categoryForReflection(child)) {
80-
importantTypes.push(child);
81-
} else {
82-
project.removeReflection(child);
83-
}
84-
});
85-
86-
internalModule.children = importantTypes;
8773
project.mergeReflections(internalModule, project);
8874
}
8975
});

plugins/processor/ownership.mjs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { ReflectionKind } from 'typedoc';
2+
3+
const HOST_KINDS = ReflectionKind.Class | ReflectionKind.Namespace;
4+
5+
const collectReferences = (type, out, depth = 0) => {
6+
if (!type || depth > 30) return;
7+
8+
const next = [];
9+
10+
switch (type.type) {
11+
case 'reference':
12+
if (type.reflection) out.add(type.reflection.id);
13+
next.push(...(type.typeArguments ?? []));
14+
break;
15+
case 'union':
16+
case 'intersection':
17+
next.push(...(type.types ?? []));
18+
break;
19+
case 'array':
20+
case 'optional':
21+
case 'rest':
22+
next.push(type.elementType);
23+
break;
24+
case 'tuple':
25+
next.push(...(type.elements ?? []));
26+
break;
27+
case 'namedTupleMember':
28+
next.push(type.element);
29+
break;
30+
case 'indexedAccess':
31+
next.push(type.objectType, type.indexType);
32+
break;
33+
case 'conditional':
34+
next.push(
35+
type.checkType,
36+
type.extendsType,
37+
type.trueType,
38+
type.falseType
39+
);
40+
break;
41+
case 'predicate':
42+
next.push(type.targetType);
43+
break;
44+
case 'query':
45+
next.push(type.queryType);
46+
break;
47+
case 'mapped':
48+
next.push(type.parameterType, type.templateType, type.nameType);
49+
break;
50+
case 'templateLiteral':
51+
next.push(...(type.tail ?? []).map(([tailType]) => tailType));
52+
break;
53+
case 'typeOperator':
54+
next.push(type.target);
55+
break;
56+
default:
57+
break;
58+
}
59+
60+
for (const child of next) collectReferences(child, out, depth + 1);
61+
};
62+
63+
// The page that hosts a referring reflection: the nearest class or namespace,
64+
// a root structural type (resolved transitively), or the project itself.
65+
// Project-level referrers still count as owners so types used by root exports
66+
// never look single-owned, but the project is never a co-location target.
67+
const hostFor = (reflection, structuralIds) => {
68+
let current = reflection;
69+
70+
while (current) {
71+
if (current.kindOf?.(HOST_KINDS)) return current;
72+
if (structuralIds.has(current.id)) return current;
73+
if (current.isProject?.()) return current;
74+
current = current.parent;
75+
}
76+
77+
return undefined;
78+
};
79+
80+
/**
81+
* Map each root structural type to the single class or namespace page that
82+
* references it, when exactly one exists.
83+
*/
84+
export const soleOwnerPages = (project, structuralTypes) => {
85+
const structuralIds = new Set(structuralTypes.map(type => type.id));
86+
const owners = new Map();
87+
const structuralReferrers = new Map();
88+
89+
for (const type of structuralTypes) {
90+
owners.set(type.id, new Set());
91+
structuralReferrers.set(type.id, new Set());
92+
}
93+
94+
for (const reflection of Object.values(project.reflections)) {
95+
const references = new Set();
96+
97+
collectReferences(reflection.type, references);
98+
collectReferences(reflection.default, references);
99+
for (const extended of reflection.extendedTypes ?? []) {
100+
collectReferences(extended, references);
101+
}
102+
for (const implemented of reflection.implementedTypes ?? []) {
103+
collectReferences(implemented, references);
104+
}
105+
106+
if (!references.size) continue;
107+
108+
const host = hostFor(reflection, structuralIds);
109+
if (!host) continue;
110+
111+
for (const id of references) {
112+
if (id === host.id || !owners.has(id)) continue;
113+
114+
if (structuralIds.has(host.id)) {
115+
structuralReferrers.get(id).add(host.id);
116+
} else {
117+
owners.get(id).add(host);
118+
}
119+
}
120+
}
121+
122+
let changed = true;
123+
while (changed) {
124+
changed = false;
125+
126+
for (const [id, referrerIds] of structuralReferrers) {
127+
const ownHosts = owners.get(id);
128+
129+
for (const referrerId of referrerIds) {
130+
for (const host of owners.get(referrerId)) {
131+
if (!ownHosts.has(host)) {
132+
ownHosts.add(host);
133+
changed = true;
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
const soleOwners = new Map();
141+
142+
for (const type of structuralTypes) {
143+
const [owner, ...rest] = owners.get(type.id);
144+
if (owner && !rest.length && owner.kindOf?.(HOST_KINDS)) {
145+
soleOwners.set(type, owner);
146+
}
147+
}
148+
149+
return soleOwners;
150+
};

plugins/processor/synthetic.mjs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Comment, DeclarationReflection, ReflectionKind } from 'typedoc';
22
import { categoryForReflection } from '../shared/categories.mjs';
33
import { setTypePageMetadata } from './metadata.mjs';
4+
import { soleOwnerPages } from './ownership.mjs';
45

56
export const TYPE_PAGE_HEADING_KINDS =
67
ReflectionKind.Interface | ReflectionKind.TypeAlias;
@@ -33,6 +34,15 @@ const typePageComment = () =>
3334
},
3435
]);
3536

37+
const attachToOwner = (owner, children) => {
38+
for (const child of children) {
39+
child.parent = owner;
40+
owner.addChild(child);
41+
}
42+
43+
owner.groups = [...(owner.groups ?? []), { title: 'Types', children }];
44+
};
45+
3646
const removeChildren = (items, moved) =>
3747
items?.filter(item => !moved.has(item));
3848

@@ -80,7 +90,7 @@ const createTypePage = (project, baseName, children) => {
8090
const compareByName = (a, b) => a.name.localeCompare(b.name);
8191

8292
/**
83-
* Move root interfaces and type aliases onto synthetic category pages so they
93+
* Move root interfaces and type aliases onto the page that owns them so they
8494
* remain linkable without appearing as root API exports.
8595
*
8696
* @param {import('typedoc').ProjectReflection} project
@@ -90,9 +100,20 @@ export const createTypePages = project => {
90100
.filter(child => child.kindOf(TYPE_PAGE_HEADING_KINDS))
91101
.sort(compareByName);
92102
const movedSet = new Set(movedTypes);
103+
const ownerPages = soleOwnerPages(project, movedTypes);
93104
const byPage = new Map();
105+
const byOwner = new Map();
94106

95107
for (const reflection of movedTypes) {
108+
const owner = ownerPages.get(reflection);
109+
110+
if (owner) {
111+
const siblings = byOwner.get(owner) ?? [];
112+
siblings.push(reflection);
113+
byOwner.set(owner, siblings);
114+
continue;
115+
}
116+
96117
const baseName = typePageBaseName(reflection);
97118
const group = byPage.get(baseName) ?? [];
98119
group.push(reflection);
@@ -109,6 +130,10 @@ export const createTypePages = project => {
109130
project.groups = removeFromGroups(project.groups, movedSet);
110131
project.categories = removeFromGroups(project.categories, movedSet);
111132

133+
for (const [owner, children] of byOwner) {
134+
attachToOwner(owner, children);
135+
}
136+
112137
return [...byPage].map(([baseName, children]) => ({
113138
baseName,
114139
children,

0 commit comments

Comments
 (0)