Преглед изворни кода

feat(docs): Allow generated TS docs to br grouped into pages

Michael Bromley пре 6 година
родитељ
комит
eba4c281b3

+ 5 - 0
docs/README.md

@@ -44,6 +44,11 @@ Currently, any `interface`, `class` or `type` which includes the JSDoc `@docCate
 
 This is required as its presence determines whether the declaration is extracted into the docs. Its value should be a string corresponding to the API sub-section that this declaration belongs to, e.g. "payment", "shipping" etc.
 
+##### `@docsPage`
+
+This optional tag can be used to group declarations together onto a single page. This is useful e.g. in the case of utility functions or
+type aliases, which may be considered too trivial to get an entire page to themselves.
+
 ##### `@description`
 
 This tag specifies the text description of the declaration. It supports markdown, but should not be used for code blocks, which should be tagged with `@example` (see below). Links to other declarations can be made with the `{@link SomeOtherDeclaration}` syntax. Also applies to class/interface members.

+ 19 - 8
scripts/docs/generate-typescript-docs.ts

@@ -65,33 +65,44 @@ function generateTypescriptDocs(config: DocsSectionConfig[], isWatchMode: boolea
     const globalTypeMap: TypeMap = new Map();
 
     if (!isWatchMode) {
-        for (const {outputPath, sourceDirs} of config) {
+        for (const { outputPath, sourceDirs } of config) {
             deleteGeneratedDocs(absOutputPath(outputPath));
         }
     }
 
     for (const { outputPath, sourceDirs } of config) {
         const sourceFilePaths = getSourceFilePaths(sourceDirs);
-        const parsedDeclarations = new TypescriptDocsParser().parse(sourceFilePaths);
-        for (const info of parsedDeclarations) {
-            const { category, fileName } = info;
-            const pathToTypeDoc = `${outputPath}/${category ? category + '/' : ''}${fileName === '_index' ? '' : fileName}`;
-            globalTypeMap.set(info.title, pathToTypeDoc);
+        const docsPages = new TypescriptDocsParser().parse(sourceFilePaths);
+        for (const page of docsPages) {
+            const { category, fileName, declarations } = page;
+            for (const declaration of declarations) {
+                const pathToTypeDoc = `${outputPath}/${category ? category + '/' : ''}${
+                    fileName === '_index' ? '' : fileName
+                }#${toHash(declaration.title)}`;
+                globalTypeMap.set(declaration.title, pathToTypeDoc);
+            }
         }
         const docsUrl = `/docs`;
         const generatedCount = new TypescriptDocsRenderer().render(
-            parsedDeclarations,
+            docsPages,
             docsUrl,
             absOutputPath(outputPath),
             globalTypeMap,
         );
 
         if (generatedCount) {
-            console.log(`Generated ${generatedCount} typescript api docs for "${outputPath}" in ${+new Date() - timeStart}ms`);
+            console.log(
+                `Generated ${generatedCount} typescript api docs for "${outputPath}" in ${+new Date() -
+                    timeStart}ms`,
+            );
         }
     }
 }
 
+function toHash(title: string): string {
+    return title.replace(/\s/g, '').toLowerCase();
+}
+
 function absOutputPath(outputPath: string): string {
     return path.join(__dirname, '../../docs/content/docs/', outputPath);
 }

+ 8 - 1
scripts/docs/typescript-docgen-types.ts

@@ -25,6 +25,13 @@ export interface MethodInfo extends MemberInfo {
     parameters: MethodParameterInfo[];
 }
 
+export interface DocsPage {
+    title: string;
+    category: string;
+    declarations: ParsedDeclaration[];
+    fileName: string;
+}
+
 export interface DeclarationInfo {
     packageName: string;
     sourceFile: string;
@@ -34,7 +41,7 @@ export interface DeclarationInfo {
     weight: number;
     category: string;
     description: string;
-    fileName: string;
+    page: string | undefined;
 }
 
 export interface InterfaceInfo extends DeclarationInfo {

+ 34 - 8
scripts/docs/typescript-docs-parser.ts

@@ -5,7 +5,7 @@ import ts from 'typescript';
 import { notNullOrUndefined } from '../../packages/common/src/shared-utils';
 
 import {
-    ClassInfo,
+    ClassInfo, DocsPage,
     InterfaceInfo,
     MemberInfo,
     MethodInfo,
@@ -27,7 +27,7 @@ export class TypescriptDocsParser {
      * Parses the TypeScript files given by the filePaths array and returns the
      * parsed data structures ready for rendering.
      */
-    parse(filePaths: string[]): ParsedDeclaration[] {
+    parse(filePaths: string[]): DocsPage[] {
         const sourceFiles = filePaths.map(filePath => {
             return ts.createSourceFile(
                 filePath,
@@ -39,7 +39,7 @@ export class TypescriptDocsParser {
 
         const statements = this.getStatementsWithSourceLocation(sourceFiles);
 
-        return statements
+        const pageMap = statements
             .map(statement => {
                 const info = this.parseDeclaration(
                     statement.statement,
@@ -48,8 +48,27 @@ export class TypescriptDocsParser {
                 );
                 return info;
             })
-            .filter(notNullOrUndefined);
-    }
+            .filter(notNullOrUndefined)
+            .reduce((pages, declaration) => {
+                const pageTitle = declaration.page || declaration.title;
+                const existingPage = pages.get(pageTitle);
+                if (existingPage) {
+                    existingPage.declarations.push(declaration);
+                } else {
+                    const normalizedTitle = this.kebabCase(pageTitle);
+                    const fileName = normalizedTitle === declaration.category ? '_index' : normalizedTitle;
+                    pages.set(pageTitle, {
+                        title: pageTitle,
+                        category: declaration.category,
+                        declarations: [declaration],
+                        fileName,
+                    });
+                }
+                return pages;
+            }, 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
@@ -92,8 +111,7 @@ export class TypescriptDocsParser {
         const fullText = this.getDeclarationFullText(statement);
         const weight = this.getDeclarationWeight(statement);
         const description = this.getDeclarationDescription(statement);
-        const normalizedTitle = this.kebabCase(title);
-        const fileName = normalizedTitle === category ? '_index' : normalizedTitle;
+        const docsPage = this.getDocsPage(statement);
         const packageName = this.getPackageName(sourceFile);
 
         const info = {
@@ -105,7 +123,7 @@ export class TypescriptDocsParser {
             weight,
             category,
             description,
-            fileName,
+            page: docsPage,
         };
 
         if (ts.isInterfaceDeclaration(statement)) {
@@ -290,6 +308,14 @@ export class TypescriptDocsParser {
         return weight;
     }
 
+    private getDocsPage(statement: ValidDeclaration): string | undefined {
+        let docsPage: string | undefined;
+        this.parseTags(statement, {
+            docsPage: tag => docsPage = tag.comment,
+        });
+        return docsPage;
+    }
+
     /**
      * Reads the @description JSDoc tag from the interface.
      */

+ 50 - 44
scripts/docs/typescript-docs-renderer.ts

@@ -9,7 +9,7 @@ import { assertNever } from '../../packages/common/src/shared-utils';
 import { deleteGeneratedDocs, generateFrontMatter } from './docgen-utils';
 import {
     ClassInfo,
-    DeclarationInfo,
+    DeclarationInfo, DocsPage,
     EnumInfo,
     FunctionInfo,
     InterfaceInfo, MethodParameterInfo,
@@ -20,45 +20,53 @@ import {
 
 export class TypescriptDocsRenderer {
 
-    render(parsedDeclarations: ParsedDeclaration[], docsUrl: string, outputPath: string, typeMap: TypeMap): number {
+    render(pages: DocsPage[], docsUrl: string, outputPath: string, typeMap: TypeMap): number {
         let generatedCount = 0;
         if (!fs.existsSync(outputPath)) {
             fs.mkdirs(outputPath);
         }
-        for (const info of parsedDeclarations) {
+
+        for (const page of pages) {
             let markdown = '';
-            switch (info.kind) {
-                case 'interface':
-                    markdown = this.renderInterfaceOrClass(info, typeMap, docsUrl);
-                    break;
-                case 'typeAlias':
-                    markdown = this.renderTypeAlias(info, typeMap, docsUrl);
-                    break;
-                case 'class':
-                    markdown = this.renderInterfaceOrClass(info, typeMap, docsUrl);
-                    break;
-                case 'enum':
-                    markdown = this.renderEnum(info, typeMap, docsUrl);
-                    break;
-                case 'function':
-                    markdown = this.renderFunction(info, typeMap, docsUrl);
-                    break;
-                default:
-                    assertNever(info);
+            markdown += generateFrontMatter(page.title, 10);
+            markdown += `\n# ${page.title}\n`;
+            for (const info of page.declarations) {
+                // markdown += `## ${info.title}\n`;
+                // markdown += '{{< declaration >}}\n';
+                switch (info.kind) {
+                    case 'interface':
+                        markdown += this.renderInterfaceOrClass(info, typeMap, docsUrl);
+                        break;
+                    case 'typeAlias':
+                        markdown += this.renderTypeAlias(info, typeMap, docsUrl);
+                        break;
+                    case 'class':
+                        markdown += this.renderInterfaceOrClass(info, typeMap, docsUrl);
+                        break;
+                    case 'enum':
+                        markdown += this.renderEnum(info, typeMap, docsUrl);
+                        break;
+                    case 'function':
+                        markdown += this.renderFunction(info, typeMap, docsUrl);
+                        break;
+                    default:
+                        assertNever(info);
+                }
+                // markdown += '{{< /declaration >}}\n';
             }
 
-            const categoryDir = path.join(outputPath, info.category);
+            const categoryDir = path.join(outputPath, page.category);
             const indexFile = path.join(categoryDir, '_index.md');
             if (!fs.existsSync(categoryDir)) {
                 fs.mkdirsSync(categoryDir);
             }
             if (!fs.existsSync(indexFile)) {
-                const indexFileContent = generateFrontMatter(info.category, 10, false) + `\n\n# ${info.category}`;
+                const indexFileContent = generateFrontMatter(page.category, 10, false) + `\n\n# ${page.category}`;
                 fs.writeFileSync(indexFile, indexFileContent);
                 generatedCount++;
             }
 
-            fs.writeFileSync(path.join(categoryDir, info.fileName + '.md'), markdown);
+            fs.writeFileSync(path.join(categoryDir, page.fileName + '.md'), markdown);
             generatedCount++;
         }
         return generatedCount;
@@ -70,14 +78,15 @@ export class TypescriptDocsRenderer {
     private renderInterfaceOrClass(info: InterfaceInfo | ClassInfo, knownTypeMap: TypeMap, docsUrl: string): string {
         const { title, weight, category, description, members } = info;
         let output = '';
-        output += generateFrontMatter(title, weight);
-        output += `\n\n# ${title}\n\n`;
+        output += `\n\n## ${title}\n\n`;
         output += this.renderGenerationInfoShortcode(info);
         output += `${this.renderDescription(description, knownTypeMap, docsUrl)}\n\n`;
-        output += `## Signature\n\n`;
+        output += `### Signature\n\n`;
         output += info.kind === 'interface' ? this.renderInterfaceSignature(info) : this.renderClassSignature(info);
-        output += `## Members\n\n`;
-        output += `${this.renderMembers(info, knownTypeMap, docsUrl)}\n`;
+        if (info.members && info.members.length) {
+            output += `### Members\n\n`;
+            output += `${this.renderMembers(info, knownTypeMap, docsUrl)}\n`;
+        }
         return output;
     }
 
@@ -87,14 +96,13 @@ export class TypescriptDocsRenderer {
     private renderTypeAlias(typeAliasInfo: TypeAliasInfo, knownTypeMap: TypeMap, docsUrl: string): string {
         const { title, weight, description, type, fullText } = typeAliasInfo;
         let output = '';
-        output += generateFrontMatter(title, weight);
-        output += `\n\n# ${title}\n\n`;
+        output += `\n\n## ${title}\n\n`;
         output += this.renderGenerationInfoShortcode(typeAliasInfo);
         output += `${this.renderDescription(description, knownTypeMap, docsUrl)}\n\n`;
-        output += `## Signature\n\n`;
+        output += `### Signature\n\n`;
         output += this.renderTypeAliasSignature(typeAliasInfo);
-        if (typeAliasInfo.members) {
-            output += `## Members\n\n`;
+        if (typeAliasInfo.members && typeAliasInfo.members.length) {
+            output += `### Members\n\n`;
             output += `${this.renderMembers(typeAliasInfo, knownTypeMap, docsUrl)}\n`;
         }
         return output;
@@ -103,11 +111,10 @@ export class TypescriptDocsRenderer {
     private renderEnum(enumInfo: EnumInfo, knownTypeMap: TypeMap, docsUrl: string): string {
         const { title, weight, description, fullText } = enumInfo;
         let output = '';
-        output += generateFrontMatter(title, weight);
-        output += `\n\n# ${title}\n\n`;
+        output += `\n\n## ${title}\n\n`;
         output += this.renderGenerationInfoShortcode(enumInfo);
         output += `${this.renderDescription(description, knownTypeMap, docsUrl)}\n\n`;
-        output += `## Signature\n\n`;
+        output += `### Signature\n\n`;
         output += this.renderEnumSignature(enumInfo);
         return output;
     }
@@ -115,14 +122,13 @@ export class TypescriptDocsRenderer {
     private renderFunction(functionInfo: FunctionInfo, knownTypeMap: TypeMap, docsUrl: string): string {
         const { title, weight, description, fullText, parameters } = functionInfo;
         let output = '';
-        output += generateFrontMatter(title, weight);
-        output += `\n\n# ${title}\n\n`;
+        output += `\n\n## ${title}\n\n`;
         output += this.renderGenerationInfoShortcode(functionInfo);
         output += `${this.renderDescription(description, knownTypeMap, docsUrl)}\n\n`;
-        output += `## Signature\n\n`;
+        output += `### Signature\n\n`;
         output += this.renderFunctionSignature(functionInfo, knownTypeMap);
         if (parameters.length) {
-            output += `## Parameters\n\n`;
+            output += `### Parameters\n\n`;
             output += this.renderFunctionParams(parameters, knownTypeMap, docsUrl);
         }
         return output;
@@ -257,9 +263,9 @@ export class TypescriptDocsRenderer {
                     type = `(${args}) => ${this.renderType(member.type, knownTypeMap, docsUrl)}`;
                 }
             }
-            output += `### ${member.name}\n\n`;
+            output += `#### ${member.name}\n\n`;
             output += `{{< member-info kind="${[...member.modifiers, member.kind].join(' ')}" type="${type}" ${defaultParam}>}}\n\n`;
-            output += `${this.renderDescription(member.description, knownTypeMap, docsUrl)}\n\n`;
+            output += `{{< member-description >}}${this.renderDescription(member.description, knownTypeMap, docsUrl)}{{< /member-description >}}\n\n`;
         }
         return output;
     }
@@ -288,7 +294,7 @@ export class TypescriptDocsRenderer {
         for (const [key, val] of knownTypeMap) {
             const re = new RegExp(`\\b${key}\\b`, 'g');
             const strippedIndex = val.replace(/\/_index$/, '');
-            typeText = typeText.replace(re, `<a href='${docsUrl}/${strippedIndex}/'>${key}</a>`);
+            typeText = typeText.replace(re, `<a href='${docsUrl}/${strippedIndex}'>${key}</a>`);
         }
         return typeText;
     }