import { SourceFile } from "./source"; import { Comments } from "./comments"; // @ts-ignore import fs from "fs"; import { parsePythonDocstrings } from "./python"; export interface ReferenceDocConfiguration { sourcePath: string; destinationPath: string; className?: string; component?: string; hook?: string; description?: string; title?: string; pythonSymbols?: string[]; typescriptSymbols?: string[]; } export class ReferenceDoc { constructor(private readonly referenceDoc: ReferenceDocConfiguration) {} async generate() { let generatedDocumentation: string | null = null; if (this.referenceDoc.pythonSymbols) { generatedDocumentation = this.generatedDocsPythonSymbols(); } else if (this.referenceDoc.typescriptSymbols) { generatedDocumentation = await this.generatedDocsTypeScriptSymbols(); } else { generatedDocumentation = await this.generatedDocsTypeScript(); } if (generatedDocumentation) { const dest = this.referenceDoc.destinationPath; // Ensure parent directory exists so retargeting to a fresh tree // (e.g. shell-docs) doesn't require manual mkdir. // eslint-disable-next-line @typescript-eslint/no-explicit-any (fs as any).mkdirSync(dest.split("/").slice(0, -1).join("/"), { recursive: true, }); fs.writeFileSync(dest, generatedDocumentation); console.log( `Successfully autogenerated ${dest} from ${this.referenceDoc.sourcePath}`, ); } } generateTitle( title: string, description: string, sourcePath: string, ): string { let result = `---\n`; result += `title: "${title}"\n`; if (description) { result += `description: "${description}"\n`; } result += `---\n\n`; result += `{\n`; result += ` /*\n`; result += ` * ATTENTION! DO NOT MODIFY THIS FILE!\n`; result += ` * This page is auto-generated. If you want to make any changes to this page, changes must be made at:\n`; result += ` * ${sourcePath}\n`; result += ` */\n`; result += `}\n`; return result; } generatedDocsPythonSymbols(): string | null { const content = fs.readFileSync(this.referenceDoc.sourcePath, "utf8"); const parsed = parsePythonDocstrings( this.referenceDoc.pythonSymbols!, content, ); let result = this.generateTitle( this.referenceDoc.title || "", this.referenceDoc.description || "", this.referenceDoc.sourcePath, ); for (const fn of this.referenceDoc.pythonSymbols!) { if (fn in parsed) { const fnDoc = parsed[fn]; result += `## ${fn}\n\n`; result += `${fnDoc.description}\n\n`; if (fnDoc.parameters) { result += `### Parameters\n\n`; for (const param of fnDoc.parameters) { const required = !param.type.startsWith("Optional"); result += ` \n`; result += `${param.description}\n`; result += `\n\n`; } } if (fnDoc.returns) { result += `### Returns\n\n`; result += `\n`; result += `${fnDoc.returns.description}\n`; result += `\n\n`; } } } return result; } async generatedDocsTypeScriptSymbols(): Promise { const source = new SourceFile(this.referenceDoc.sourcePath); await source.parse(); let result = this.generateTitle( this.referenceDoc.title || "", this.referenceDoc.description || "", this.referenceDoc.sourcePath, ); for (const fn of this.referenceDoc.typescriptSymbols!) { const args = source.getFunctionArguments(fn); if (!args) { console.warn(`${fn} not found in ${this.referenceDoc.sourcePath}`); continue; } const comment = source.getFunctionComment(fn); result += `## ${fn}\n\n`; if (comment) { result += `${comment}\n\n`; } if (args) { result += `### Parameters\n\n`; for (const arg of args) { result += ` \n`; result += `${arg.description}\n`; result += `\n\n`; } } } return result; } async generatedDocsTypeScript(): Promise { const source = new SourceFile(this.referenceDoc.sourcePath); await source.parse(); const comment = Comments.getFirstCommentBlock(source.sourceFile); if (!comment) { console.warn(`No comment found for ${this.referenceDoc.sourcePath}`); console.warn("Skipping..."); return null; } // handle imports const slashes = this.referenceDoc.destinationPath.split("/").length; let importPathPrefix = ""; for (let i = 0; i < slashes - 2; i++) { importPathPrefix += "../"; } let result = this.generateTitle( this.referenceDoc.className || this.referenceDoc.component || this.referenceDoc.hook || "", this.referenceDoc.description || "", this.referenceDoc.sourcePath, ); result += `${comment}\n\n`; const arg0Interface = await source.getArg0Interface( this.referenceDoc.className || this.referenceDoc.component || this.referenceDoc.hook || "", ); if (arg0Interface) { const hasProperties = arg0Interface.properties.length > 0; if (this.referenceDoc.hook && hasProperties) { result += `## Parameters\n\n`; } else if (this.referenceDoc.component && hasProperties) { result += `## Properties\n\n`; } else if (this.referenceDoc.className && hasProperties) { result += `## Constructor Parameters\n\n`; } for (const property of arg0Interface.properties) { if (property.comment.includes("@deprecated")) { continue; } const type = property.type.replace(/"/g, "'"); result += ` \n`; result += `${property.comment}\n`; result += `\n\n`; } } else if (this.referenceDoc.className) { const constr = source.getConstructorDefinition( this.referenceDoc.className, ); if (constr) { result += `## ${constr.signature}\n\n`; result += `${constr.comment}\n\n`; for (const param of constr.parameters) { const type = param.type.replace(/"/g, "'"); result += `\n`; result += `${param.comment}\n`; result += `\n\n`; } } } if (this.referenceDoc.className) { const methodDefinitions = await source.getPublicMethodDefinitions( this.referenceDoc.className, ); for (const method of methodDefinitions) { if ( method.signature === "process(request: CopilotRuntimeChatCompletionRequest)" || method.signature === "process(request: CopilotRuntimeRequest)" ) { // skip the process method continue; } const methodName = method.signature.split("(")[0]; const methodArgs = method.signature.split("(")[1].split(")")[0]; result += `\n`; result += `${method.comment}\n\n`; for (const param of method.parameters) { const type = param.type.replace(/"/g, "'"); result += ` \n`; result += ` ${param.comment}\n`; result += ` \n\n`; } result += `\n\n`; } } return result; } }