Skip to content

Commit eab3a8b

Browse files
committed
chore: add autogenerated docs for component adapters
1 parent f5f2a44 commit eab3a8b

File tree

14 files changed

+1380
-124
lines changed

14 files changed

+1380
-124
lines changed

build/componentDocsEmitter.cjs

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
// This script generates UI component prop documentation using ts-morph (CommonJS version).
2+
// It will flatten all props, including those picked from React types, and output markdown in the same format as component-inventory.md.
3+
4+
const { Project } = require('ts-morph')
5+
const { writeFile, mkdir } = require('fs').promises
6+
const { join } = require('path')
7+
8+
const UI_DIR = join(__dirname, '../src/components/Common/UI')
9+
const OUTPUT_DIR = join(__dirname, '../docs/06/01')
10+
const OUTPUT_FILE = join(OUTPUT_DIR, 'component-inventory.md')
11+
const OMIT = [
12+
'BaseListProps',
13+
'SharedFieldLayoutProps',
14+
'FieldLayoutProps',
15+
'InternalFieldLayoutProps',
16+
'SharedHorizontalFieldLayoutProps',
17+
'HorizontalFieldLayoutProps',
18+
'InputProps',
19+
]
20+
21+
function getAllPropsFlattened(interfaceOrAlias) {
22+
return interfaceOrAlias.getType().getApparentProperties()
23+
}
24+
25+
function formatType(type) {
26+
if (!type) return '-'
27+
const typeText = type.getText()
28+
if (typeText === 'ReactNode' || typeText === 'React.ReactNode') {
29+
return typeText
30+
}
31+
32+
if (typeText.includes('ReactNode')) {
33+
return typeText.includes('React.ReactNode') ? 'React.ReactNode' : 'ReactNode'
34+
}
35+
36+
if (type.isArray()) {
37+
return formatType(type.getArrayElementType()) + '[]'
38+
}
39+
40+
if (type.isUnion()) {
41+
const unionTypes = type.getUnionTypes().filter(t => t.getText() !== 'undefined')
42+
// Special-case React Ref<T> union: null | function | RefObject<T>
43+
const hasNull = unionTypes.some(t => t.getText() === 'null')
44+
const refObjType = unionTypes.find(t => {
45+
const symbol = t.getSymbol && t.getSymbol()
46+
return symbol && symbol.getName() === 'RefObject'
47+
})
48+
49+
const fnType = unionTypes.find(t => t.getCallSignatures && t.getCallSignatures().length === 1)
50+
if (hasNull && refObjType && fnType && unionTypes.length === 3) {
51+
// Try to get the type argument from RefObject<T>
52+
const typeArgs = refObjType.getTypeArguments && refObjType.getTypeArguments()
53+
if (typeArgs && typeArgs.length) {
54+
// Render type argument as plain text, not a link
55+
const argType = typeArgs[0]
56+
const argName = argType.getSymbol()?.getName() || argType.getText()
57+
return `Ref<${argName}>`
58+
}
59+
return 'Ref<any>'
60+
}
61+
62+
if (
63+
unionTypes.length === 2 &&
64+
((unionTypes[0].getText() === 'false' && unionTypes[1].getText() === 'true') ||
65+
(unionTypes[0].getText() === 'true' && unionTypes[1].getText() === 'false'))
66+
) {
67+
return 'boolean'
68+
}
69+
70+
if (unionTypes.length === 1) {
71+
return formatType(unionTypes[0])
72+
}
73+
74+
return unionTypes.map(formatType).join(' | ')
75+
}
76+
77+
if (type.isIntersection()) {
78+
return type.getIntersectionTypes().map(formatType).join(' & ')
79+
}
80+
81+
// Special-case RefObject and Ref
82+
const symbol = type.getSymbol && type.getSymbol()
83+
if (symbol) {
84+
const name = symbol.getName()
85+
if (name === 'RefObject' || name === 'Ref') {
86+
const typeArgs = type.getTypeArguments()
87+
if (typeArgs.length) {
88+
// Render type argument as plain text, not a link
89+
const argType = typeArgs[0]
90+
const argName = argType.getSymbol()?.getName() || argType.getText()
91+
return `${name}<${argName}>`
92+
}
93+
return name
94+
}
95+
}
96+
97+
if (symbol && symbol.getDeclarations().length) {
98+
const decl = symbol.getDeclarations()[0]
99+
if (
100+
decl.getKindName() === 'InterfaceDeclaration' ||
101+
decl.getKindName() === 'TypeAliasDeclaration'
102+
) {
103+
const name = symbol.getName()
104+
105+
if (name === 'RefObject') {
106+
return name
107+
}
108+
109+
return `[${name}](#${name.toLowerCase()})`
110+
}
111+
}
112+
return typeText
113+
}
114+
115+
function escapePipes(str) {
116+
return str.replace(/\|/g, '\\|')
117+
}
118+
119+
function getDefaultValue(prop, decl) {
120+
if (decl && decl.getKindName() === 'PropertySignature') {
121+
const initializer = decl.getInitializer && decl.getInitializer()
122+
if (initializer) return initializer.getText()
123+
}
124+
return '-'
125+
}
126+
127+
function getDescription(prop, decl) {
128+
if (!decl) return '-'
129+
const jsDocs = decl.getJsDocs && decl.getJsDocs()
130+
if (jsDocs && jsDocs.length > 0) {
131+
return (
132+
jsDocs
133+
.map(doc => doc.getComment() || '')
134+
.join(' ')
135+
.trim() || '-'
136+
)
137+
}
138+
return '-'
139+
}
140+
141+
function findReferencedNames(type, allNames) {
142+
if (type.isArray()) {
143+
return findReferencedNames(type.getArrayElementType(), allNames)
144+
}
145+
146+
if (type.isUnion()) {
147+
return type.getUnionTypes().flatMap(t => findReferencedNames(t, allNames))
148+
}
149+
150+
if (type.isIntersection()) {
151+
return type.getIntersectionTypes().flatMap(t => findReferencedNames(t, allNames))
152+
}
153+
154+
const symbol = type.getSymbol && type.getSymbol()
155+
if (symbol && allNames.has(symbol.getName())) {
156+
return [symbol.getName()]
157+
}
158+
159+
return []
160+
}
161+
162+
async function main() {
163+
const project = new Project({
164+
tsConfigFilePath: join(__dirname, '../tsconfig.json'),
165+
skipAddingFilesFromTsConfig: false,
166+
})
167+
168+
// Add all *Types.ts files in UI_DIR
169+
project.addSourceFilesAtPaths(join(UI_DIR, '**/*Types.ts'))
170+
// Add React types for prop flattening
171+
project.addSourceFilesAtPaths(join(__dirname, '../node_modules/@types/react/index.d.ts'))
172+
173+
// Get all interfaces and type aliases in the UI directory
174+
const sourceFiles = project.getSourceFiles(join(UI_DIR, '**/*Types.ts'))
175+
const interfaces = sourceFiles
176+
.flatMap(sourceFile => sourceFile.getInterfaces())
177+
.filter(interfaceDecl => !OMIT.includes(interfaceDecl.getName()))
178+
179+
// --- Add type aliases that resolve to object types ---
180+
const typeAliases = sourceFiles
181+
.flatMap(sourceFile => sourceFile.getTypeAliases())
182+
.filter(typeAlias => {
183+
const type = typeAlias.getType()
184+
// Type literal or intersection of object types
185+
if (type.isObject() && type.getProperties().length > 0) {
186+
return true
187+
}
188+
189+
if (type.isIntersection()) {
190+
return type.getIntersectionTypes().some(t => t.isObject() && t.getProperties().length > 0)
191+
}
192+
193+
return false
194+
})
195+
.filter(typeAlias => !OMIT.includes(typeAlias.getName()))
196+
197+
// Wrap type aliases in a similar interface-like object for unified handling
198+
const pseudoInterfaces = typeAliases.map(typeAlias => ({
199+
getName: () => typeAlias.getName(),
200+
getType: () => typeAlias.getType(),
201+
getDeclarations: () => [typeAlias],
202+
}))
203+
204+
// Combine interfaces and pseudo-interfaces
205+
const allInterfaces = [...interfaces, ...pseudoInterfaces].sort((a, b) =>
206+
a.getName().localeCompare(b.getName()),
207+
)
208+
209+
// --- Infer parent-child relationships ---
210+
const interfaceMap = new Map(allInterfaces.map(intf => [intf.getName(), intf]))
211+
const allNames = new Set(allInterfaces.map(intf => intf.getName()))
212+
const parentToChildren = new Map()
213+
const childToParent = new Map()
214+
215+
for (const parentIntf of allInterfaces) {
216+
const props = getAllPropsFlattened(parentIntf)
217+
for (const prop of props) {
218+
const referencedNames = findReferencedNames(prop.getTypeAtLocation(parentIntf), allNames)
219+
for (const typeName of referencedNames) {
220+
if (!parentToChildren.has(parentIntf.getName())) {
221+
parentToChildren.set(parentIntf.getName(), [])
222+
}
223+
224+
if (!parentToChildren.get(parentIntf.getName()).includes(typeName)) {
225+
parentToChildren.get(parentIntf.getName()).push(typeName)
226+
}
227+
228+
childToParent.set(typeName, parentIntf.getName())
229+
}
230+
}
231+
}
232+
233+
// --- Prepare output order ---
234+
// Only top-level parents (not children) go in the top-level list
235+
const topLevelInterfaces = allInterfaces.filter(intf => !childToParent.has(intf.getName()))
236+
237+
// --- Topological sort for logical order ---
238+
// Build dependency graph: parent -> [children]
239+
const dependencyGraph = new Map()
240+
allInterfaces.forEach(intf => {
241+
dependencyGraph.set(intf.getName(), parentToChildren.get(intf.getName()) || [])
242+
})
243+
244+
// Topological sort
245+
const visited = new Set()
246+
const sortedNames = []
247+
248+
function visit(name) {
249+
if (visited.has(name)) {
250+
return
251+
}
252+
253+
visited.add(name)
254+
const children = dependencyGraph.get(name) || []
255+
for (const child of children) {
256+
visit(child)
257+
}
258+
sortedNames.push(name)
259+
}
260+
261+
// Visit all interfaces (in original order for stability)
262+
allInterfaces.forEach(intf => visit(intf.getName()))
263+
264+
// --- Compute heading levels ---
265+
// Start at level 2 for top-level, children are one deeper than their parent
266+
const headingLevels = {}
267+
function setHeadingLevel(name, level) {
268+
headingLevels[name] = level
269+
const children = dependencyGraph.get(name) || []
270+
for (const child of children) {
271+
setHeadingLevel(child, level + 1)
272+
}
273+
}
274+
275+
topLevelInterfaces.forEach(intf => setHeadingLevel(intf.getName(), 2))
276+
277+
// --- Generate index (nested) ---
278+
function indexEntry(intf, level = 0, indexed = new Set()) {
279+
if (indexed.has(intf.getName())) {
280+
return ''
281+
}
282+
283+
indexed.add(intf.getName())
284+
285+
const indent = ' '.repeat(level)
286+
let entry = `${indent}- [${intf.getName()}](#${intf.getName().toLowerCase()})`
287+
const children = parentToChildren.get(intf.getName()) || []
288+
for (const childName of children) {
289+
const childIntf = interfaceMap.get(childName)
290+
if (childIntf) {
291+
const childEntry = indexEntry(childIntf, level + 1, indexed)
292+
if (childEntry) {
293+
entry += `\n${childEntry}`
294+
}
295+
}
296+
}
297+
return entry
298+
}
299+
const indexSet = new Set()
300+
const index = topLevelInterfaces
301+
.map(intf => indexEntry(intf, 0, indexSet))
302+
.filter(Boolean)
303+
.join('\n')
304+
305+
// --- Output sections in parent-before-children order ---
306+
const outputted = new Set()
307+
function sectionForInterfaceParentFirst(intf, level = 2) {
308+
if (outputted.has(intf.getName())) {
309+
return ''
310+
}
311+
312+
const heading = `${'#'.repeat(level)} ${intf.getName()}`
313+
const props = getAllPropsFlattened(intf)
314+
// Remove duplicates by name
315+
const seen = new Set()
316+
const uniqueProps = props.filter(p => {
317+
if (seen.has(p.getName())) return false
318+
seen.add(p.getName())
319+
return true
320+
})
321+
const rows = uniqueProps.map(prop => {
322+
const name = prop.getName()
323+
324+
let type = formatType(prop.getTypeAtLocation(intf))
325+
type = type.replace(/\n/g, ' ').replace(/\r/g, ' ')
326+
const decl = prop.getDeclarations()[0]
327+
328+
const required =
329+
decl && typeof decl.hasQuestionToken === 'function'
330+
? decl.hasQuestionToken()
331+
? 'No'
332+
: 'Yes'
333+
: '-'
334+
335+
const defaultValue = getDefaultValue(prop, decl)
336+
const description = getDescription(prop, decl)
337+
const isMarkdownLink = type.startsWith('[') && type.includes('](')
338+
const typeCell = isMarkdownLink ? escapePipes(type) : `\`${escapePipes(type)}\``
339+
340+
return `| **${name}** | ${typeCell} | ${required} | ${defaultValue} | ${description} |`
341+
})
342+
const header =
343+
'| Prop | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|'
344+
outputted.add(intf.getName())
345+
let section = `${heading}\n\n${header}\n${rows.join('\n')}`
346+
// Output children after parent
347+
const children = parentToChildren.get(intf.getName()) || []
348+
for (const childName of children) {
349+
const childIntf = interfaceMap.get(childName)
350+
if (childIntf) {
351+
const childSection = sectionForInterfaceParentFirst(childIntf, level + 1)
352+
if (childSection) {
353+
section += '\n\n' + childSection
354+
}
355+
}
356+
}
357+
return section
358+
}
359+
const sections = topLevelInterfaces
360+
.map(intf => sectionForInterfaceParentFirst(intf))
361+
.filter(Boolean)
362+
.join('\n\n')
363+
364+
const markdown = `# Component Inventory\n\n${index}\n\n${sections}`
365+
366+
await mkdir(OUTPUT_DIR, { recursive: true })
367+
await writeFile(OUTPUT_FILE, markdown)
368+
console.log(`Documentation written to ${OUTPUT_FILE}`)
369+
}
370+
371+
main().catch(console.error)

build/typedocMdPrefixPlugin.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function load(app) {
2+
app.renderer.markdownHooks.on('page.begin', args => {
3+
const name = args.page.model.name
4+
const title = name.includes('/') ? name.split('/').pop() : name
5+
return `# ${title}`
6+
}),
7+
app.renderer.markdownHooks.on('index.page.begin', args => {
8+
return `# Component Adapter Inventory
9+
10+
The following are a list of types for available component adapters. Follow the links below to view the interfaces.`
11+
})
12+
}

0 commit comments

Comments
 (0)