Skip to content

Commit d054ab2

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

File tree

12 files changed

+1266
-110
lines changed

12 files changed

+1266
-110
lines changed

build/componentDocsEmitter.cjs

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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+
// --- CONFIG ---
9+
const UI_DIR = join(__dirname, '../src/components/Common/UI')
10+
const OUTPUT_DIR = join(__dirname, '../docs/06/01')
11+
const OUTPUT_FILE = join(OUTPUT_DIR, 'component-inventory.md')
12+
const OMIT = [
13+
'BaseListProps',
14+
'SharedFieldLayoutProps',
15+
'FieldLayoutProps',
16+
'InternalFieldLayoutProps',
17+
'SharedHorizontalFieldLayoutProps',
18+
'HorizontalFieldLayoutProps',
19+
'InputProps',
20+
]
21+
22+
function getAllPropsFlattened(intf) {
23+
return intf.getType().getApparentProperties()
24+
}
25+
26+
function formatType(type) {
27+
if (!type) return '-'
28+
const text = type.getText()
29+
if (text === 'ReactNode' || text === 'React.ReactNode') return text
30+
if (text.includes('ReactNode'))
31+
return text.includes('React.ReactNode') ? 'React.ReactNode' : 'ReactNode'
32+
if (type.isArray()) return formatType(type.getArrayElementType()) + '[]'
33+
if (type.isUnion()) {
34+
const filtered = type.getUnionTypes().filter(t => t.getText() !== 'undefined')
35+
if (
36+
filtered.length === 2 &&
37+
((filtered[0].getText() === 'false' && filtered[1].getText() === 'true') ||
38+
(filtered[0].getText() === 'true' && filtered[1].getText() === 'false'))
39+
) {
40+
return 'boolean'
41+
}
42+
if (filtered.length === 1) return formatType(filtered[0])
43+
return filtered.map(formatType).join(' | ')
44+
}
45+
if (type.isIntersection()) return type.getIntersectionTypes().map(formatType).join(' & ')
46+
if (type.getSymbol() && type.getSymbol().getDeclarations().length) {
47+
const decl = type.getSymbol().getDeclarations()[0]
48+
if (
49+
decl.getKindName() === 'InterfaceDeclaration' ||
50+
decl.getKindName() === 'TypeAliasDeclaration'
51+
) {
52+
const name = type.getSymbol().getName()
53+
if (name === 'RefObject') return name
54+
return `[${name}](#${name.toLowerCase()})`
55+
}
56+
}
57+
return text
58+
}
59+
60+
function escapePipes(str) {
61+
return str.replace(/\|/g, '\\|')
62+
}
63+
64+
function getDefaultValue(prop, decl) {
65+
if (decl && decl.getKindName() === 'PropertySignature') {
66+
const init = decl.getInitializer && decl.getInitializer()
67+
if (init) return init.getText()
68+
}
69+
return '-'
70+
}
71+
72+
function getDescription(prop, decl) {
73+
if (!decl) return '-'
74+
const jsDocs = decl.getJsDocs && decl.getJsDocs()
75+
if (jsDocs && jsDocs.length > 0) {
76+
return (
77+
jsDocs
78+
.map(doc => doc.getComment() || '')
79+
.join(' ')
80+
.trim() || '-'
81+
)
82+
}
83+
return '-'
84+
}
85+
86+
function findReferencedNames(type, allNames) {
87+
if (type.isArray()) return findReferencedNames(type.getArrayElementType(), allNames)
88+
if (type.isUnion()) return type.getUnionTypes().flatMap(t => findReferencedNames(t, allNames))
89+
if (type.isIntersection())
90+
return type.getIntersectionTypes().flatMap(t => findReferencedNames(t, allNames))
91+
const symbol = type.getSymbol && type.getSymbol()
92+
if (symbol && allNames.has(symbol.getName())) return [symbol.getName()]
93+
return []
94+
}
95+
96+
async function main() {
97+
const project = new Project({
98+
tsConfigFilePath: join(__dirname, '../tsconfig.json'),
99+
skipAddingFilesFromTsConfig: false,
100+
})
101+
102+
// Add all *Types.ts files in UI_DIR
103+
project.addSourceFilesAtPaths(join(UI_DIR, '**/*Types.ts'))
104+
// Add React types for prop flattening
105+
project.addSourceFilesAtPaths(join(__dirname, '../node_modules/@types/react/index.d.ts'))
106+
107+
// Get all interfaces and type aliases in the UI directory
108+
const sourceFiles = project.getSourceFiles(join(UI_DIR, '**/*Types.ts'))
109+
const interfaces = sourceFiles
110+
.flatMap(sf => sf.getInterfaces())
111+
.filter(intf => !OMIT.includes(intf.getName()))
112+
113+
// --- Add type aliases that resolve to object types ---
114+
const typeAliases = sourceFiles
115+
.flatMap(sf => sf.getTypeAliases())
116+
.filter(ta => {
117+
// Only include type aliases that resolve to object types
118+
const type = ta.getType()
119+
// Type literal or intersection of object types
120+
if (type.isObject() && type.getProperties().length > 0) return true
121+
if (type.isIntersection()) {
122+
return type.getIntersectionTypes().some(t => t.isObject() && t.getProperties().length > 0)
123+
}
124+
return false
125+
})
126+
.filter(ta => !OMIT.includes(ta.getName()))
127+
128+
// Wrap type aliases in a similar interface-like object for unified handling
129+
const pseudoInterfaces = typeAliases.map(ta => ({
130+
getName: () => ta.getName(),
131+
getType: () => ta.getType(),
132+
getDeclarations: () => [ta],
133+
}))
134+
135+
// Combine interfaces and pseudo-interfaces
136+
const allIntfs = [...interfaces, ...pseudoInterfaces].sort((a, b) =>
137+
a.getName().localeCompare(b.getName()),
138+
)
139+
140+
// --- Infer parent-child relationships ---
141+
const interfaceMap = new Map(allIntfs.map(intf => [intf.getName(), intf]))
142+
const allNames = new Set(allIntfs.map(intf => intf.getName()))
143+
const parentToChildren = new Map()
144+
const childToParent = new Map()
145+
146+
for (const parentIntf of allIntfs) {
147+
const props = getAllPropsFlattened(parentIntf)
148+
for (const prop of props) {
149+
const referenced = findReferencedNames(prop.getTypeAtLocation(parentIntf), allNames)
150+
for (const typeName of referenced) {
151+
if (!parentToChildren.has(parentIntf.getName()))
152+
parentToChildren.set(parentIntf.getName(), [])
153+
if (!parentToChildren.get(parentIntf.getName()).includes(typeName)) {
154+
parentToChildren.get(parentIntf.getName()).push(typeName)
155+
}
156+
childToParent.set(typeName, parentIntf.getName())
157+
}
158+
}
159+
}
160+
161+
// --- Prepare output order ---
162+
// Only top-level parents (not children) go in the top-level list
163+
const topLevelInterfaces = allIntfs.filter(intf => !childToParent.has(intf.getName()))
164+
165+
// --- Topological sort for logical order ---
166+
// Build dependency graph: parent -> [children]
167+
const graph = new Map()
168+
allIntfs.forEach(intf => {
169+
graph.set(intf.getName(), parentToChildren.get(intf.getName()) || [])
170+
})
171+
172+
// Topological sort
173+
const visited = new Set()
174+
const sorted = []
175+
function visit(name) {
176+
if (visited.has(name)) return
177+
visited.add(name)
178+
const children = graph.get(name) || []
179+
for (const child of children) {
180+
visit(child)
181+
}
182+
sorted.push(name)
183+
}
184+
// Visit all interfaces (in original order for stability)
185+
allIntfs.forEach(intf => visit(intf.getName()))
186+
187+
// --- Compute heading levels ---
188+
// Start at level 2 for top-level, children are one deeper than their parent
189+
const headingLevels = {}
190+
function setHeadingLevel(name, level) {
191+
headingLevels[name] = level
192+
const children = graph.get(name) || []
193+
for (const child of children) {
194+
setHeadingLevel(child, level + 1)
195+
}
196+
}
197+
topLevelInterfaces.forEach(intf => setHeadingLevel(intf.getName(), 2))
198+
199+
// --- Generate index (nested) ---
200+
function indexEntry(intf, level = 0, indexed = new Set()) {
201+
if (indexed.has(intf.getName())) return ''
202+
indexed.add(intf.getName())
203+
const indent = ' '.repeat(level)
204+
let entry = `${indent}- [${intf.getName()}](#${intf.getName().toLowerCase()})`
205+
const children = parentToChildren.get(intf.getName()) || []
206+
for (const childName of children) {
207+
const childIntf = interfaceMap.get(childName)
208+
if (childIntf) {
209+
const childEntry = indexEntry(childIntf, level + 1, indexed)
210+
if (childEntry) entry += `\n${childEntry}`
211+
}
212+
}
213+
return entry
214+
}
215+
const indexSet = new Set()
216+
const index = topLevelInterfaces
217+
.map(intf => indexEntry(intf, 0, indexSet))
218+
.filter(Boolean)
219+
.join('\n')
220+
221+
// --- Output sections in parent-before-children order ---
222+
const outputted = new Set()
223+
function sectionForInterfaceParentFirst(intf, level = 2) {
224+
if (outputted.has(intf.getName())) return ''
225+
const heading = `${'#'.repeat(level)} ${intf.getName()}`
226+
const props = getAllPropsFlattened(intf)
227+
// Remove duplicates by name
228+
const seen = new Set()
229+
const uniqueProps = props.filter(p => {
230+
if (seen.has(p.getName())) return false
231+
seen.add(p.getName())
232+
return true
233+
})
234+
const rows = uniqueProps.map(prop => {
235+
const name = prop.getName()
236+
let type = formatType(prop.getTypeAtLocation(intf))
237+
type = type.replace(/\n/g, ' ').replace(/\r/g, ' ')
238+
const decl = prop.getDeclarations()[0]
239+
const required =
240+
decl && typeof decl.hasQuestionToken === 'function'
241+
? decl.hasQuestionToken()
242+
? 'No'
243+
: 'Yes'
244+
: '-'
245+
const def = getDefaultValue(prop, decl)
246+
const desc = getDescription(prop, decl)
247+
const isMarkdownLink = type.startsWith('[') && type.includes('](')
248+
const typeCell = isMarkdownLink ? escapePipes(type) : `\`${escapePipes(type)}\``
249+
return `| **${name}** | ${typeCell} | ${required} | ${def} | ${desc} |`
250+
})
251+
const header =
252+
'| Prop | Type | Required | Default | Description |\n|------|------|----------|---------|-------------|'
253+
outputted.add(intf.getName())
254+
let section = `${heading}\n\n${header}\n${rows.join('\n')}`
255+
// Output children after parent
256+
const children = parentToChildren.get(intf.getName()) || []
257+
for (const childName of children) {
258+
const childIntf = interfaceMap.get(childName)
259+
if (childIntf) {
260+
const childSection = sectionForInterfaceParentFirst(childIntf, level + 1)
261+
if (childSection) section += '\n\n' + childSection
262+
}
263+
}
264+
return section
265+
}
266+
const sections = topLevelInterfaces
267+
.map(intf => sectionForInterfaceParentFirst(intf))
268+
.filter(Boolean)
269+
.join('\n\n')
270+
271+
const markdown = `# Component Inventory\n\n${index}\n\n${sections}`
272+
273+
await mkdir(OUTPUT_DIR, { recursive: true })
274+
await writeFile(OUTPUT_FILE, markdown)
275+
console.log(`Documentation written to ${OUTPUT_FILE}`)
276+
}
277+
278+
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)