485 lines
17 KiB
JavaScript
485 lines
17 KiB
JavaScript
/*
|
|
* Copyright Elasticsearch B.V. and contributors
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import path from 'node:path'
|
|
import fs from 'node:fs/promises'
|
|
import { rimraf } from 'rimraf'
|
|
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()
|
|
|
|
const docsDir = path.join(import.meta.dirname, '..', 'docs', 'reference', 'api')
|
|
|
|
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'
|
|
code += generatePropertyType(spec.excerptTokens)
|
|
code += '\n'
|
|
code += '```\n'
|
|
return code
|
|
}
|
|
|
|
function generateInterface(spec) {
|
|
let code = `# \`${spec.displayName}\` [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 : ' '}`
|
|
code += ' |\n'
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
function generateClass(spec) {
|
|
let code = `# \`${spec.displayName}\` [class-${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 += generatePropertyType(con.excerptTokens).replace(/^constructor/, `new ${spec.displayName}`)
|
|
code += '\n'
|
|
code += '```\n\n'
|
|
}
|
|
|
|
// generate properties
|
|
const props = spec.members.filter(m => m.kind === 'Property')
|
|
if (props.length > 0) {
|
|
code += `\n## Properties [class-properties-${spec.displayName}]\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 : ' '}`
|
|
code += ' |\n'
|
|
}
|
|
}
|
|
|
|
if (spec.displayName === 'AsyncSearch') console.log(spec)
|
|
|
|
// generate methods
|
|
const methods = spec.members
|
|
.filter(m => m.kind === 'Method')
|
|
// condense methods with multiple signatures down to one listing
|
|
.reduce((a, b) => {
|
|
if (a[b.displayName] == null) {
|
|
a[b.displayName] = [b]
|
|
} else {
|
|
a[b.displayName].push(b)
|
|
}
|
|
return a
|
|
}, {})
|
|
|
|
if (Object.keys(methods).length > 0) {
|
|
code += `\n## Methods [class-methods-${spec.displayName}]\n\n`
|
|
code += '| Name | Signature | Description |\n'
|
|
code += '| - | - | - |\n'
|
|
|
|
for (const method of Object.keys(methods)) {
|
|
// const methodInfo = methods[method]
|
|
code += `| \`${method}\` | client.${method}(params, transportOptions) | n/a |\n`
|
|
// code += ` | \`${generatePropertyType(method.excerptTokens)}\``
|
|
// const description = generateDescription(method.tsdocComment, false)
|
|
// code += ` | ${description.length > 0 ? description : ' '}`
|
|
// code += ' |\n'
|
|
}
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
function generateAlias(spec) {
|
|
let code = `# \`${spec.displayName}\` [alias-${spec.displayName}]\n`
|
|
code += '```typescript\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(docsDir, `${name}.md`)
|
|
await fs.writeFile(filePath, code, 'utf8')
|
|
}
|
|
|
|
async function start() {
|
|
// clear any existing files
|
|
await rimraf(docsDir)
|
|
await fs.mkdir(docsDir, { recursive: true })
|
|
|
|
const model = new Extractor.ApiModel()
|
|
const pkg = model.loadPackage(path.join(import.meta.dirname, '..', 'api-extractor', 'elasticsearch.api.json'))
|
|
const entry = pkg.entryPoints[0]
|
|
|
|
const lookup = (canonicalReference) => entry.members.find(m => m.canonicalReference.toString().startsWith(canonicalReference))
|
|
|
|
// creating our own custom tree structure to more cleanly represent the available functions as "methods" of `Client`
|
|
const clientEntry = lookup('@elastic/elasticsearch!Client:class')
|
|
const client = {
|
|
entry: clientEntry,
|
|
members: {
|
|
constructor: clientEntry.members.find(m => m.kind === 'Constructor'),
|
|
methods: {},
|
|
apis: {},
|
|
helpers: {}
|
|
}
|
|
}
|
|
|
|
for (const member of clientEntry.members.filter(m => m.kind === 'Method')) {
|
|
client.members.methods[member.displayName] = member
|
|
}
|
|
|
|
// add API functions to client
|
|
const api = lookup('@elastic/elasticsearch!~API:interface')
|
|
for (const prop of api.members.filter(m => m.kind === 'PropertySignature')) {
|
|
const reference = prop.excerptTokens.find(p => p.kind === 'Reference')
|
|
const canonicalReference = reference.canonicalReference.toString()
|
|
|
|
if (canonicalReference.endsWith(':class')) {
|
|
// expand out to all class methods
|
|
const parent = {}
|
|
const classEntry = lookup(canonicalReference)
|
|
for (const method of classEntry.members.filter(m => m.kind === 'Method')) {
|
|
if (parent[method.name] == null) {
|
|
parent[method.name] = method
|
|
}
|
|
}
|
|
client.members.apis[prop.name] = parent
|
|
} else if (canonicalReference.endsWith(':function')) {
|
|
// add member as a single function
|
|
const functionEntry = lookup(canonicalReference)
|
|
client.members.apis[prop.name] = functionEntry
|
|
}
|
|
}
|
|
|
|
// TODO: add helper methods to client
|
|
// const helpers = lookup('@elastic/elasticsearch!~Helpers:class')
|
|
// for (const prop of helpers.members.filter(m => m.kind === 'Method')) {
|
|
// const reference = prop.excerptTokens.find(p => p.kind === 'Reference')
|
|
// client.members.helpers[prop.name] = reference.canonicalReference.toString()
|
|
// for (const exc of prop.excerptTokens.filter(m => m.kind === 'Reference')) {
|
|
// console.log(exc.canonicalReference.toString())
|
|
// }
|
|
// }
|
|
|
|
await renderClientDoc(client)
|
|
for (const apiName of Object.keys(client.members.apis)) {
|
|
const api = client.members.apis[apiName]
|
|
await renderApiDoc(apiName, api)
|
|
}
|
|
|
|
async function renderClientDoc(client) {
|
|
let code = '# Client [client]\n\n'
|
|
|
|
const { constructor } = client.members
|
|
code += `## Constructor\n\n`
|
|
code += '```typescript\n'
|
|
code += `new Client(`
|
|
code += constructor.parameters.map(renderParam).join(', ')
|
|
code += ')\n'
|
|
code += '```\n\n'
|
|
code += '### Arguments\n\n'
|
|
code += renderArgTable(constructor.parameters)
|
|
code += '\n'
|
|
|
|
const { methods } = client.members
|
|
code += `## Methods\n\n`
|
|
Object.keys(methods).forEach(method => {
|
|
code += `### \`${method}\`\n\n`
|
|
code += generateDescription(methods[method].tsdocComment) + '\n\n'
|
|
|
|
if (methods[method].parameters.length > 0) {
|
|
code += '### Arguments\n\n'
|
|
code += renderArgTable(methods[method].parameters)
|
|
// TODO: add return type
|
|
code += '\n'
|
|
}
|
|
})
|
|
|
|
const { apis } = client.members
|
|
code += `## Elasticsearch APIs\n\n`
|
|
Object.keys(apis).forEach(api => {
|
|
code += `- [\`${api}\`](./api-${api}.md)\n`
|
|
})
|
|
|
|
await write('Client', code)
|
|
}
|
|
|
|
async function renderApiDoc(name, api) {
|
|
let code = `# \`${name}\` [api-${name}]\n\n`
|
|
|
|
if (api.kind === 'Function') {
|
|
code += generateDescription(api.tsdocComment) + '\n\n'
|
|
// TODO: generate function code snippet
|
|
code += '## Arguments\n\n'
|
|
code += renderArgTable(api.parameters.slice(1, 3), true) + '\n'
|
|
// TODO: Inject `params` type definition
|
|
// TODO: add return type
|
|
} else {
|
|
for (const apiName of Object.keys(api)) {
|
|
code += `## \`${name}.${apiName}\` [api-${name}.${apiName}]\n\n`
|
|
// TODO: generate function code snippet
|
|
code += generateDescription(api[apiName].tsdocComment) + '\n\n'
|
|
code += '### Arguments\n\n'
|
|
code += renderArgTable(api[apiName].parameters.slice(1, 3), true) + '\n'
|
|
// TODO: Inject `params` type definition
|
|
// TODO: add return type
|
|
}
|
|
}
|
|
|
|
await write(`api-${name}`, code)
|
|
}
|
|
}
|
|
|
|
function renderParam(param) {
|
|
const type = param.parameterTypeExcerpt.tokens.find(t => t.kind === 'Reference').text
|
|
return `${param.name}${param.isOptional ? '?' : ''}: ${type}`
|
|
}
|
|
|
|
function renderLinkedType(token) {
|
|
const type = token.text
|
|
return `[\`${type}\`](./${type}.md)`
|
|
}
|
|
|
|
function renderArgTable(params, apiFunction = false) {
|
|
let code = '| Name | Type | Description |\n'
|
|
code += '| - | - | - |\n'
|
|
|
|
params.forEach(p => {
|
|
let type = renderLinkedType(p.parameterTypeExcerpt.spannedTokens[0])
|
|
let desc = ' '
|
|
if (apiFunction) {
|
|
if (p.name === 'params') {
|
|
desc = 'Request parameters for this API call'
|
|
type = type.replace(/\bT\./g, '')
|
|
} else if (p.name === 'options') {
|
|
desc = 'Overrides client HTTP request settings just for this request'
|
|
type = '[`TransportRequestOptions`](./TransportRequestOptions.md)'
|
|
}
|
|
} else {
|
|
const maybeDesc = generateDescription(p.tsdocComment)
|
|
desc = maybeDesc.length > 0 ? maybeDesc : desc
|
|
}
|
|
code += `| ${p.name} | ${type} | ${desc} |\n`
|
|
})
|
|
|
|
return code
|
|
}
|
|
|
|
|
|
start()
|
|
.then(() => process.exit(0))
|
|
.catch(err => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
})
|