/* * 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 = ` ` 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: <>): 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) })