Просмотр исходного кода

feat(docs): Display heritage data in generated TS docs

Closes #326
Michael Bromley 5 лет назад
Родитель
Сommit
2f6b002833

+ 4 - 4
scripts/docs/typescript-docgen-types.ts

@@ -1,4 +1,4 @@
-import ts from 'typescript';
+import ts, { HeritageClause } from 'typescript';
 
 export interface MethodParameterInfo {
     name: string;
@@ -46,14 +46,14 @@ export interface DeclarationInfo {
 
 export interface InterfaceInfo extends DeclarationInfo {
     kind: 'interface';
-    extends?: string;
+    extendsClause: HeritageClause | undefined;
     members: Array<PropertyInfo | MethodInfo>;
 }
 
 export interface ClassInfo extends DeclarationInfo {
     kind: 'class';
-    implements?: string;
-    extends?: string;
+    extendsClause: HeritageClause | undefined;
+    implementsClause: HeritageClause | undefined;
     members: Array<PropertyInfo | MethodInfo>;
 }
 

+ 44 - 40
scripts/docs/typescript-docs-parser.ts

@@ -1,11 +1,12 @@
 import fs from 'fs';
 import path from 'path';
-import ts from 'typescript';
+import ts, { HeritageClause } from 'typescript';
 
 import { notNullOrUndefined } from '../../packages/common/src/shared-utils';
 
 import {
-    ClassInfo, DocsPage,
+    ClassInfo,
+    DocsPage,
     InterfaceInfo,
     MemberInfo,
     MethodInfo,
@@ -28,7 +29,7 @@ export class TypescriptDocsParser {
      * parsed data structures ready for rendering.
      */
     parse(filePaths: string[]): DocsPage[] {
-        const sourceFiles = filePaths.map(filePath => {
+        const sourceFiles = filePaths.map((filePath) => {
             return ts.createSourceFile(
                 filePath,
                 this.replaceEscapedAtTokens(fs.readFileSync(filePath).toString()),
@@ -40,7 +41,7 @@ export class TypescriptDocsParser {
         const statements = this.getStatementsWithSourceLocation(sourceFiles);
 
         const pageMap = statements
-            .map(statement => {
+            .map((statement) => {
                 const info = this.parseDeclaration(
                     statement.statement,
                     statement.sourceFile,
@@ -68,7 +69,7 @@ export class TypescriptDocsParser {
             }, new Map<string, DocsPage>());
 
         return Array.from(pageMap.values());
-    };
+    }
 
     /**
      * Maps an array of parsed SourceFiles into statements, including a reference to the original file each statement
@@ -77,19 +78,14 @@ export class TypescriptDocsParser {
     private getStatementsWithSourceLocation(
         sourceFiles: ts.SourceFile[],
     ): Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }> {
-        return sourceFiles.reduce(
-            (st, sf) => {
-                const statementsWithSources = sf.statements.map(statement => {
-                    const sourceFile = path
-                        .relative(path.join(__dirname, '..'), sf.fileName)
-                        .replace(/\\/g, '/');
-                    const sourceLine = sf.getLineAndCharacterOfPosition(statement.getStart()).line + 1;
-                    return { statement, sourceFile, sourceLine };
-                });
-                return [...st, ...statementsWithSources];
-            },
-            [] as Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }>,
-        );
+        return sourceFiles.reduce((st, sf) => {
+            const statementsWithSources = sf.statements.map((statement) => {
+                const sourceFile = path.relative(path.join(__dirname, '..'), sf.fileName).replace(/\\/g, '/');
+                const sourceLine = sf.getLineAndCharacterOfPosition(statement.getStart()).line + 1;
+                return { statement, sourceFile, sourceLine };
+            });
+            return [...st, ...statementsWithSources];
+        }, [] as Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }>);
     }
 
     /**
@@ -135,7 +131,7 @@ export class TypescriptDocsParser {
             return {
                 ...info,
                 kind: 'interface',
-                extends: this.getHeritageClauseText(statement, ts.SyntaxKind.ExtendsKeyword),
+                extendsClause: this.getHeritageClause(statement, ts.SyntaxKind.ExtendsKeyword),
                 members: this.parseMembers(statement.members),
             };
         } else if (ts.isTypeAliasDeclaration(statement)) {
@@ -152,8 +148,8 @@ export class TypescriptDocsParser {
                 ...info,
                 kind: 'class',
                 members: this.parseMembers(statement.members),
-                extends: this.getHeritageClauseText(statement, ts.SyntaxKind.ExtendsKeyword),
-                implements: this.getHeritageClauseText(statement, ts.SyntaxKind.ImplementsKeyword),
+                extendsClause: this.getHeritageClause(statement, ts.SyntaxKind.ExtendsKeyword),
+                implementsClause: this.getHeritageClause(statement, ts.SyntaxKind.ImplementsKeyword),
             };
         } else if (ts.isEnumDeclaration(statement)) {
             return {
@@ -162,7 +158,7 @@ export class TypescriptDocsParser {
                 members: this.parseMembers(statement.members) as PropertyInfo[],
             };
         } else if (ts.isFunctionDeclaration(statement)) {
-            const parameters = statement.parameters.map(p => ({
+            const parameters = statement.parameters.map((p) => ({
                 name: p.name.getText(),
                 type: p.type ? p.type.getText() : '',
                 optional: !!p.questionToken,
@@ -185,19 +181,19 @@ export class TypescriptDocsParser {
     /**
      * Returns the text of any "extends" or "implements" clause of a class or interface.
      */
-    private getHeritageClauseText(
+    private getHeritageClause(
         statement: ts.ClassDeclaration | ts.InterfaceDeclaration,
         kind: ts.SyntaxKind.ExtendsKeyword | ts.SyntaxKind.ImplementsKeyword,
-    ): string | undefined {
+    ): HeritageClause | undefined {
         const { heritageClauses } = statement;
         if (!heritageClauses) {
             return;
         }
-        const clause = heritageClauses.find(cl => cl.token === kind);
+        const clause = heritageClauses.find((cl) => cl.token === kind);
         if (!clause) {
             return;
         }
-        return clause.getText();
+        return clause;
     }
 
     /**
@@ -211,8 +207,12 @@ export class TypescriptDocsParser {
             name = declaration.name ? declaration.name.getText() : 'anonymous';
         }
         let typeParams = '';
-        if (!ts.isEnumDeclaration(declaration) && !ts.isVariableStatement(declaration) && declaration.typeParameters) {
-            typeParams = '<' + declaration.typeParameters.map(tp => tp.getText()).join(', ') + '>';
+        if (
+            !ts.isEnumDeclaration(declaration) &&
+            !ts.isVariableStatement(declaration) &&
+            declaration.typeParameters
+        ) {
+            typeParams = '<' + declaration.typeParameters.map((tp) => tp.getText()).join(', ') + '>';
         }
         return name + typeParams;
     }
@@ -235,7 +235,7 @@ export class TypescriptDocsParser {
         const result: Array<PropertyInfo | MethodInfo> = [];
 
         for (const member of members) {
-            const modifiers = member.modifiers ? member.modifiers.map(m => m.getText()) : [];
+            const modifiers = member.modifiers ? member.modifiers.map((m) => m.getText()) : [];
             const isPrivate = modifiers.includes('private');
             if (
                 !isPrivate &&
@@ -248,7 +248,11 @@ export class TypescriptDocsParser {
                     ts.isGetAccessorDeclaration(member) ||
                     ts.isIndexSignatureDeclaration(member))
             ) {
-                const name = member.name ? member.name.getText() : ts.isIndexSignatureDeclaration(member) ? '[index]' : 'constructor';
+                const name = member.name
+                    ? member.name.getText()
+                    : ts.isIndexSignatureDeclaration(member)
+                    ? '[index]'
+                    : 'constructor';
                 let description = '';
                 let type = '';
                 let defaultValue = '';
@@ -265,10 +269,10 @@ export class TypescriptDocsParser {
                     fullText = member.getText();
                 }
                 this.parseTags(member, {
-                    description: tag => (description += tag.comment || ''),
-                    example: tag => (description += this.formatExampleCode(tag.comment)),
-                    default: tag => (defaultValue = tag.comment || ''),
-                    internal: tag => (isInternal = true),
+                    description: (tag) => (description += tag.comment || ''),
+                    example: (tag) => (description += this.formatExampleCode(tag.comment)),
+                    default: (tag) => (defaultValue = tag.comment || ''),
+                    internal: (tag) => (isInternal = true),
                 });
                 if (isInternal) {
                     continue;
@@ -288,7 +292,7 @@ export class TypescriptDocsParser {
                     ts.isMethodDeclaration(member) ||
                     ts.isConstructorDeclaration(member)
                 ) {
-                    parameters = member.parameters.map(p => ({
+                    parameters = member.parameters.map((p) => ({
                         name: p.name.getText(),
                         type: p.type ? p.type.getText() : '',
                         optional: !!p.questionToken,
@@ -318,7 +322,7 @@ export class TypescriptDocsParser {
     private getDeclarationWeight(statement: ValidDeclaration): number {
         let weight = 10;
         this.parseTags(statement, {
-            docsWeight: tag => (weight = Number.parseInt(tag.comment || '10', 10)),
+            docsWeight: (tag) => (weight = Number.parseInt(tag.comment || '10', 10)),
         });
         return weight;
     }
@@ -326,7 +330,7 @@ export class TypescriptDocsParser {
     private getDocsPage(statement: ValidDeclaration): string | undefined {
         let docsPage: string | undefined;
         this.parseTags(statement, {
-            docsPage: tag => docsPage = tag.comment,
+            docsPage: (tag) => (docsPage = tag.comment),
         });
         return docsPage;
     }
@@ -337,8 +341,8 @@ export class TypescriptDocsParser {
     private getDeclarationDescription(statement: ValidDeclaration): string {
         let description = '';
         this.parseTags(statement, {
-            description: tag => (description += tag.comment),
-            example: tag => (description += this.formatExampleCode(tag.comment)),
+            description: (tag) => (description += tag.comment),
+            example: (tag) => (description += this.formatExampleCode(tag.comment)),
         });
         return this.restoreAtTokens(description);
     }
@@ -349,7 +353,7 @@ export class TypescriptDocsParser {
     private getDocsCategory(statement: ValidDeclaration): string | undefined {
         let category: string | undefined;
         this.parseTags(statement, {
-            docsCategory: tag => (category = tag.comment || ''),
+            docsCategory: (tag) => (category = tag.comment || ''),
         });
         return this.kebabCase(category);
     }

+ 70 - 32
scripts/docs/typescript-docs-renderer.ts

@@ -1,6 +1,7 @@
 // tslint:disable:no-console
 import fs from 'fs-extra';
 import path from 'path';
+import { HeritageClause } from 'typescript';
 
 import { assertNever } from '../../packages/common/src/shared-utils';
 
@@ -19,7 +20,6 @@ import {
 } from './typescript-docgen-types';
 
 export class TypescriptDocsRenderer {
-
     render(pages: DocsPage[], docsUrl: string, outputPath: string, typeMap: TypeMap): number {
         let generatedCount = 0;
         if (!fs.existsSync(outputPath)) {
@@ -62,7 +62,8 @@ export class TypescriptDocsRenderer {
                 fs.mkdirsSync(categoryDir);
             }
             if (!fs.existsSync(indexFile)) {
-                const indexFileContent = generateFrontMatter(page.category, 10, false) + `\n\n# ${page.category}`;
+                const indexFileContent =
+                    generateFrontMatter(page.category, 10, false) + `\n\n# ${page.category}`;
                 fs.writeFileSync(indexFile, indexFileContent);
                 generatedCount++;
             }
@@ -76,14 +77,27 @@ export class TypescriptDocsRenderer {
     /**
      * Render the interface to a markdown string.
      */
-    private renderInterfaceOrClass(info: InterfaceInfo | ClassInfo, knownTypeMap: TypeMap, docsUrl: string): string {
+    private renderInterfaceOrClass(
+        info: InterfaceInfo | ClassInfo,
+        knownTypeMap: TypeMap,
+        docsUrl: string,
+    ): string {
         const { title, weight, category, description, members } = info;
         let output = '';
         output += `\n\n# ${title}\n\n`;
         output += this.renderGenerationInfoShortcode(info);
         output += `${this.renderDescription(description, knownTypeMap, docsUrl)}\n\n`;
         output += `## Signature\n\n`;
-        output += info.kind === 'interface' ? this.renderInterfaceSignature(info) : this.renderClassSignature(info);
+        output +=
+            info.kind === 'interface' ? this.renderInterfaceSignature(info) : this.renderClassSignature(info);
+        if (info.extendsClause) {
+            output += `## Extends\n\n`;
+            output += `${this.renderHeritageClause(info.extendsClause, knownTypeMap, docsUrl)}\n`;
+        }
+        if (info.kind === 'class' && info.implementsClause) {
+            output += `## Implements\n\n`;
+            output += `${this.renderHeritageClause(info.implementsClause, knownTypeMap, docsUrl)}\n`;
+        }
         if (info.members && info.members.length) {
             output += `## Members\n\n`;
             output += `${this.renderMembers(info, knownTypeMap, docsUrl)}\n`;
@@ -152,11 +166,11 @@ export class TypescriptDocsRenderer {
         let output = '';
         output += `\`\`\`TypeScript\n`;
         output += `interface ${fullText} `;
-        if (interfaceInfo.extends) {
-            output += interfaceInfo.extends + ' ';
+        if (interfaceInfo.extendsClause) {
+            output += interfaceInfo.extendsClause.getText() + ' ';
         }
         output += `{\n`;
-        output += members.map(member => `  ${member.fullText}`).join(`\n`);
+        output += members.map((member) => `  ${member.fullText}`).join(`\n`);
         output += `\n}\n`;
         output += `\`\`\`\n`;
 
@@ -168,24 +182,24 @@ export class TypescriptDocsRenderer {
         let output = '';
         output += `\`\`\`TypeScript\n`;
         output += `class ${fullText} `;
-        if (classInfo.extends) {
-            output += classInfo.extends + ' ';
+        if (classInfo.extendsClause) {
+            output += classInfo.extendsClause.getText() + ' ';
         }
-        if (classInfo.implements) {
-            output += classInfo.implements + ' ';
+        if (classInfo.implementsClause) {
+            output += classInfo.implementsClause.getText() + ' ';
         }
         output += `{\n`;
-        const renderModifiers = (modifiers: string[]) => modifiers.length ? modifiers.join(' ') + ' ' : '';
+        const renderModifiers = (modifiers: string[]) => (modifiers.length ? modifiers.join(' ') + ' ' : '');
         output += members
-            .map(member => {
+            .map((member) => {
                 if (member.kind === 'method') {
-                    const args = member.parameters
-                        .map(p => this.renderParameter(p, p.type))
-                        .join(', ');
+                    const args = member.parameters.map((p) => this.renderParameter(p, p.type)).join(', ');
                     if (member.fullText === 'constructor') {
                         return `  constructor(${args})`;
                     } else {
-                        return `  ${renderModifiers(member.modifiers)}${member.fullText}(${args}) => ${member.type};`;
+                        return `  ${renderModifiers(member.modifiers)}${member.fullText}(${args}) => ${
+                            member.type
+                        };`;
                     }
                 } else {
                     return `  ${renderModifiers(member.modifiers)}${member.fullText}`;
@@ -205,7 +219,7 @@ export class TypescriptDocsRenderer {
         output += `type ${fullText} = `;
         if (members) {
             output += `{\n`;
-            output += members.map(member => `  ${member.fullText}`).join(`\n`);
+            output += members.map((member) => `  ${member.fullText}`).join(`\n`);
             output += `\n}\n`;
         } else {
             output += type.getText() + `\n`;
@@ -221,11 +235,13 @@ export class TypescriptDocsRenderer {
         output += `enum ${fullText} `;
         if (members) {
             output += `{\n`;
-            output += members.map(member => {
-                let line = member.description ? `  // ${member.description}\n` : '';
-                line += `  ${member.fullText}`;
-                return line;
-            }).join(`\n`);
+            output += members
+                .map((member) => {
+                    let line = member.description ? `  // ${member.description}\n` : '';
+                    line += `  ${member.fullText}`;
+                    return line;
+                })
+                .join(`\n`);
             output += `\n}\n`;
         }
         output += `\`\`\`\n`;
@@ -234,7 +250,7 @@ export class TypescriptDocsRenderer {
 
     private renderFunctionSignature(functionInfo: FunctionInfo, knownTypeMap: TypeMap): string {
         const { fullText, parameters, type } = functionInfo;
-        const args = parameters.map(p => this.renderParameter(p, p.type)).join(', ');
+        const args = parameters.map((p) => this.renderParameter(p, p.type)).join(', ');
         let output = '';
         output += `\`\`\`TypeScript\n`;
         output += `function ${fullText}(${args}): ${type ? type.getText() : 'void'}\n`;
@@ -242,7 +258,11 @@ export class TypescriptDocsRenderer {
         return output;
     }
 
-    private renderFunctionParams(params: MethodParameterInfo[], knownTypeMap: TypeMap, docsUrl: string): string {
+    private renderFunctionParams(
+        params: MethodParameterInfo[],
+        knownTypeMap: TypeMap,
+        docsUrl: string,
+    ): string {
         let output = '';
         for (const param of params) {
             const type = this.renderType(param.type, knownTypeMap, docsUrl);
@@ -252,7 +272,11 @@ export class TypescriptDocsRenderer {
         return output;
     }
 
-    private renderMembers(info: InterfaceInfo | ClassInfo | TypeAliasInfo | EnumInfo, knownTypeMap: TypeMap, docsUrl: string): string {
+    private renderMembers(
+        info: InterfaceInfo | ClassInfo | TypeAliasInfo | EnumInfo,
+        knownTypeMap: TypeMap,
+        docsUrl: string,
+    ): string {
         const { members, title } = info;
         let output = '';
         for (const member of members || []) {
@@ -265,7 +289,7 @@ export class TypescriptDocsRenderer {
                     : '';
             } else {
                 const args = member.parameters
-                    .map(p => this.renderParameter(p, this.renderType(p.type, knownTypeMap, docsUrl)))
+                    .map((p) => this.renderParameter(p, this.renderType(p.type, knownTypeMap, docsUrl)))
                     .join(', ');
                 if (member.fullText === 'constructor') {
                     type = `(${args}) => ${title}`;
@@ -274,14 +298,29 @@ export class TypescriptDocsRenderer {
                 }
             }
             output += `### ${member.name}\n\n`;
-            output += `{{< member-info kind="${[...member.modifiers, member.kind].join(' ')}" type="${type}" ${defaultParam}>}}\n\n`;
-            output += `{{< member-description >}}${this.renderDescription(member.description, knownTypeMap, docsUrl)}{{< /member-description >}}\n\n`;
+            output += `{{< member-info kind="${[...member.modifiers, member.kind].join(
+                ' ',
+            )}" type="${type}" ${defaultParam}>}}\n\n`;
+            output += `{{< member-description >}}${this.renderDescription(
+                member.description,
+                knownTypeMap,
+                docsUrl,
+            )}{{< /member-description >}}\n\n`;
         }
         return output;
     }
 
+    private renderHeritageClause(clause: HeritageClause, knownTypeMap: TypeMap, docsUrl: string) {
+        return (
+            clause.types.map((t) => ` * ${this.renderType(t.getText(), knownTypeMap, docsUrl)}`).join('\n') +
+            '\n\n'
+        );
+    }
+
     private renderParameter(p: MethodParameterInfo, typeString: string): string {
-        return `${p.name}${p.optional ? '?' : ''}: ${typeString}${p.initializer ? ` = ${p.initializer}` : ''}`;
+        return `${p.name}${p.optional ? '?' : ''}: ${typeString}${
+            p.initializer ? ` = ${p.initializer}` : ''
+        }`;
     }
 
     private renderGenerationInfoShortcode(info: DeclarationInfo): string {
@@ -297,7 +336,7 @@ export class TypescriptDocsRenderer {
         let typeText = type
             .trim()
             // encode HTML entities
-            .replace(/[\u00A0-\u9999<>\&]/gim, i => '&#' + i.charCodeAt(0) + ';')
+            .replace(/[\u00A0-\u9999<>\&]/gim, (i) => '&#' + i.charCodeAt(0) + ';')
             // remove newlines
             .replace(/\n/g, ' ');
 
@@ -319,5 +358,4 @@ export class TypescriptDocsRenderer {
         }
         return description;
     }
-
 }