Skip to content

Commit d2dd369

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

File tree

12 files changed

+1279
-110
lines changed

12 files changed

+1279
-110
lines changed

build/componentDocsEmitter.cjs

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