WIP better generated API reference docs

This commit is contained in:
Josh Mock
2025-05-27 15:30:21 -05:00
parent a1dc6f55ee
commit b627037af1
3745 changed files with 30211 additions and 15508 deletions

343
scripts/docgen.mjs Normal file
View File

@ -0,0 +1,343 @@
/*
* Copyright Elasticsearch B.V. and contributors
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path'
import fs from 'node:fs/promises'
import * as Extractor from '@microsoft/api-extractor-model'
const header = `<!--
===========================================================================================================================
|| ||
|| ||
|| ||
|| ██████╗ ███████╗ █████╗ ██████╗ ███╗ ███╗███████╗ ||
|| ██╔══██╗██╔════╝██╔══██╗██╔══██╗████╗ ████║██╔════╝ ||
|| ██████╔╝█████╗ ███████║██║ ██║██╔████╔██║█████╗ ||
|| ██╔══██╗██╔══╝ ██╔══██║██║ ██║██║╚██╔╝██║██╔══╝ ||
|| ██║ ██║███████╗██║ ██║██████╔╝██║ ╚═╝ ██║███████╗ ||
|| ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝╚══════╝ ||
|| ||
|| ||
|| This file is autogenerated, DO NOT send pull requests that changes this file directly. ||
|| You should update the script that does the generation, which can be found in scripts/docgen.mjs. ||
|| ||
|| ||
|| ||
===========================================================================================================================
-->
`
const linkedRefs = new Set()
const documented = new Set()
function nodesToText(nodes) {
let text = ''
for (const node of nodes) {
if (node.kind === 'Paragraph') {
for (const pNode of node.nodes) {
if (pNode.text) {
text += pNode.text + ' '
} else if (pNode.kind === 'CodeSpan') {
text += '`' + pNode.code + '`'
}
}
}
}
text = text.replace(/\s+/g, ' ')
return text
}
const skippableReferences = [
'Record',
'URL',
'Array',
'Promise',
'inspect.custom',
'http.IncomingHttpHeaders',
]
function generatePropertyType(tokens) {
let code = ''
tokens.forEach(token => {
if (token.kind === 'Reference' && !skippableReferences.includes(token.text)) {
let { text } = token
if (text.startsWith('T.')) {
text = text.split('.')[1]
}
linkedRefs.add(text)
code += `[${text}](./${text}.md)`
} else {
code += token.text.replace(/\n/g, '')
}
})
return code.replace(/^export (declare )?/, '').replace(/\s+/g, ' ').trim()
}
function generateDescription(comment) {
let code = ''
if (comment == null) return code
const { summarySection, customBlocks } = comment
if (summarySection != null || customBlocks != null) {
if (summarySection != null) {
const summary = nodesToText(summarySection.nodes)
code += `${summary}\n\n`
}
if (customBlocks != null) {
let defaultValue = ''
for (const block of customBlocks) {
if (block.blockTag.tagNameWithUpperCase === '@DEFAULTVALUE') {
defaultValue = nodesToText(block.content.nodes)
}
}
if (defaultValue.length > 0) {
code += `Default value: ${defaultValue}`
}
}
}
return code.trim()
}
function generateApiFunction(spec) {
// let code = '```typescript\n'
let code = ':::\n'
code += generatePropertyType(spec.excerptTokens)
code += '\n'
code += ':::\n'
return code
}
function generateInterface(spec) {
let code = `## Interface \`${spec.displayName}\`\n\n`
code += '| Name | Type | Description |\n'
code += '| - | - | - |\n'
for (const member of spec.members) {
if (member.propertyTypeExcerpt == null) continue
code += `| \`${member.displayName}\``
code += ` | ${generatePropertyType(member.propertyTypeExcerpt.spannedTokens)}`
const description = generateDescription(member.tsdocComment, false)
code += ` | ${description.length > 0 ? description : '&nbsp;'}`
code += ' |\n'
}
return code
}
function generateClass(spec) {
let code = `## \`${spec.displayName}\`\n`
code += '\n### Constructor\n\n'
const cons = spec.members.filter(m => m.kind === 'Constructor')
for (const con of cons) {
// code += '```typescript\n'
code += ':::\n'
code += generatePropertyType(con.excerptTokens).replace(/^constructor/, `new ${spec.displayName}`)
code += '\n'
code += ':::\n'
}
// generate properties
const props = spec.members.filter(m => m.kind === 'Property')
if (props.length > 0) {
code += '\n### Properties\n\n'
code += '| Name | Type | Description |\n'
code += '| - | - | - |\n'
for (const prop of props) {
if (prop.propertyTypeExcerpt == null) continue
if (prop.displayName.startsWith('[k')) continue
code += `| \`${prop.displayName}\``
code += ` | ${generatePropertyType(prop.propertyTypeExcerpt.spannedTokens)}`
const description = generateDescription(prop.tsdocComment, false)
code += ` | ${description.length > 0 ? description : '&nbsp;'}`
code += ' |\n'
}
}
// generate methods
const methods = spec.members.filter(m => m.kind === 'Method')
if (methods.length > 0) {
code += '\n### Methods\n\n'
code += '| Name | Signature | Description |\n'
code += '| - | - | - |\n'
for (const method of methods) {
code += `| \`${method.displayName}\``
code += ` | \`${generatePropertyType(method.excerptTokens)}\``
const description = generateDescription(method.tsdocComment, false)
code += ` | ${description.length > 0 ? description : '&nbsp;'}`
code += ' |'
}
}
return code
}
function generateAlias(spec) {
let code = `## \`${spec.displayName}\`\n`
// code += '```typescript\n'
code += ':::\n'
code += `${generatePropertyType(spec.excerpt.tokens)}\n`
code += ':::\n'
return code
}
/**
* Generates documentation for ClientOptions interface
* @param spec {Extractor.ApiItem}
* @returns {string} markdown content
*/
// function generateClientOptions (spec) {
// let code = `[reference-client-options-interface]\n\n== ClientOptions\n\n${header}\n\n`
// code += `[[${spec.displayName}]]\n`
// code += `=== ${spec.displayName}\n\n`
// code += generateInterface(spec)
// return code
// }
// const standardTypes = {
// 'TlsConnectionOptions': 'https://nodejs.org/api/tls.html#tlsconnectoptions-callback[Node.js TLS connection options]',
// }
/**
* @param spec {Extractor.ApiItem}
* @param model {Extractor.ApiModel}
* @returns string
*/
// function generateClientOptionsReference (spec, model) {
// let code = `${header}\n\n`
// for (const member of spec.members) {
// for (const token of member.excerptTokens) {
// if (token.kind === 'Reference' && !documented.has(token.text)) {
// documented.add(token.text)
// code += `[discrete]\n`
// code += `[[${token.text}]]\n`
// code += `=== ${token.text}\n\n`
//
// const item = model.packages[0].entryPoints[0].members.find(member => member.displayName === token.text)
// if (item != null) {
// code += generateDescription(item.tsdocComment, false)
// switch (item.kind) {
// case 'Interface':
// code += generateInterface(item)
// break;
// case 'TypeAlias':
// code += generateAlias(item)
// break
// case 'Class':
// console.log('Class', token.text)
// code += generateClass(item)
// break
// default:
// code += 'Undocumented type\n'
// break
// }
// } else if (standardTypes[token.text] != null) {
// code += `${standardTypes[token.text]}\n`
// } else {
// code += 'Unknown\n'
// }
// code += '\n'
// }
// }
// }
// return code
// }
/**
* Generates documentation for the Client class
* @param spec {Extractor.ApiItem}
* @returns {string} Asciidoc markup
*/
// function generateClientDocs (spec) {
// let code = `[reference-client-class]\n\n== Client\n\n${header}\n\n`
//
// // generate constructor and client options
// code += '[discrete]\n'
// code += '=== Constructor\n\n'
// code += '[source,ts,subs=+macros]\n'
// code += '----\n'
// code += 'new Client(options: <<ClientOptions>>): Client\n'
// code += '----\n\n'
//
// // generate methods
// code += '[discrete]\n'
// code += '=== Methods\n\n'
// for (const method of spec.members.filter(m => m.kind === 'Method')) {
// code += `[[Client.${method.displayName}]]\n`
// code += '[discrete]\n'
// code += `==== Client.${method.displayName}\n\n`
// code += 'TODO\n\n'
// }
//
// // generate properties
// code += '[discrete]\n'
// code += '=== Properties\n\n'
// for (const prop of spec.members.filter(m => m.kind === 'Property')) {
// code += `[[Client.${prop.displayName}]]\n`
// code += '[discrete]\n'
// code += `==== Client.${prop.displayName}\n\n`
// code += 'TODO\n\n'
// }
//
// return code
// }
async function write(name, code) {
const filePath = path.join(import.meta.dirname, '..', 'docs', 'reference', 'api', `${name}.md`)
await fs.writeFile(filePath, code, 'utf8')
}
async function start() {
const model = new Extractor.ApiModel()
const pkg = model.loadPackage(path.join(import.meta.dirname, '..', 'api-extractor', 'elasticsearch.api.json'))
const entry = pkg.entryPoints[0]
for (const member of entry.members) {
if (member.displayName.endsWith('_2')) continue
switch (member.kind) {
case 'Class':
await write(member.displayName, generateClass(member))
break
case 'Interface':
await write(member.displayName, generateInterface(member))
break
case 'TypeAlias':
await write(member.displayName, generateAlias(member))
break
case 'Function':
if (member.fileUrlPath.startsWith('lib/api/api')) {
// if (member.displayName === 'CountApi') console.log(member)
// TODO: drop this: That stuff
// TODO: sub name with `client.foo.bar`
await write(`${member.displayName}_${member.overloadIndex ?? ''}`, generateApiFunction(member))
// TODO: generate rollup page for each override
}
// console.log(member)
// process.exit(0)
break
case 'Namespace':
case 'Variable':
break
default:
console.log('unsupported type', member.kind, member.displayName)
break
}
// TODO: generate rollup page that includes a whole API namespace's functions, requests, responses
}
}
start()
.then(() => process.exit(0))
.catch(err => {
console.error(err)
process.exit(1)
})