Ver código fonte

docs: Add support for @since tag to indicate minimum version for APIs

Michael Bromley 4 anos atrás
pai
commit
2b20863b04

+ 4 - 0
docs/README.md

@@ -86,6 +86,10 @@ This is used to exclude members from appearing in the docs. For example, a class
 public method for internal use, but this method is not intended to be used by external consumers of that
 public method for internal use, but this method is not intended to be used by external consumers of that
 class.
 class.
 
 
+##### `@since`
+
+The @since tag indicates that a class, method, or other symbol was added in a specific version.
+
 ##### Example
 ##### Example
 
 
 ````ts
 ````ts

+ 2 - 0
scripts/docs/typescript-docgen-types.ts

@@ -13,6 +13,7 @@ export interface MemberInfo {
     type: string;
     type: string;
     fullText: string;
     fullText: string;
     modifiers: string[];
     modifiers: string[];
+    since: string | undefined;
 }
 }
 
 
 export interface PropertyInfo extends MemberInfo {
 export interface PropertyInfo extends MemberInfo {
@@ -42,6 +43,7 @@ export interface DeclarationInfo {
     category: string;
     category: string;
     description: string;
     description: string;
     page: string | undefined;
     page: string | undefined;
+    since: string | undefined;
 }
 }
 
 
 export interface InterfaceInfo extends DeclarationInfo {
 export interface InterfaceInfo extends DeclarationInfo {

+ 33 - 17
scripts/docs/typescript-docs-parser.ts

@@ -29,7 +29,7 @@ export class TypescriptDocsParser {
      * parsed data structures ready for rendering.
      * parsed data structures ready for rendering.
      */
      */
     parse(filePaths: string[]): DocsPage[] {
     parse(filePaths: string[]): DocsPage[] {
-        const sourceFiles = filePaths.map((filePath) => {
+        const sourceFiles = filePaths.map(filePath => {
             return ts.createSourceFile(
             return ts.createSourceFile(
                 filePath,
                 filePath,
                 this.replaceEscapedAtTokens(fs.readFileSync(filePath).toString()),
                 this.replaceEscapedAtTokens(fs.readFileSync(filePath).toString()),
@@ -41,7 +41,7 @@ export class TypescriptDocsParser {
         const statements = this.getStatementsWithSourceLocation(sourceFiles);
         const statements = this.getStatementsWithSourceLocation(sourceFiles);
 
 
         const pageMap = statements
         const pageMap = statements
-            .map((statement) => {
+            .map(statement => {
                 const info = this.parseDeclaration(
                 const info = this.parseDeclaration(
                     statement.statement,
                     statement.statement,
                     statement.sourceFile,
                     statement.sourceFile,
@@ -79,7 +79,7 @@ export class TypescriptDocsParser {
         sourceFiles: ts.SourceFile[],
         sourceFiles: ts.SourceFile[],
     ): Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }> {
     ): Array<{ statement: ts.Statement; sourceFile: string; sourceLine: number }> {
         return sourceFiles.reduce((st, sf) => {
         return sourceFiles.reduce((st, sf) => {
-            const statementsWithSources = sf.statements.map((statement) => {
+            const statementsWithSources = sf.statements.map(statement => {
                 const sourceFile = path.relative(path.join(__dirname, '..'), sf.fileName).replace(/\\/g, '/');
                 const sourceFile = path.relative(path.join(__dirname, '..'), sf.fileName).replace(/\\/g, '/');
                 const sourceLine = sf.getLineAndCharacterOfPosition(statement.getStart()).line + 1;
                 const sourceLine = sf.getLineAndCharacterOfPosition(statement.getStart()).line + 1;
                 return { statement, sourceFile, sourceLine };
                 return { statement, sourceFile, sourceLine };
@@ -113,6 +113,7 @@ export class TypescriptDocsParser {
         const weight = this.getDeclarationWeight(statement);
         const weight = this.getDeclarationWeight(statement);
         const description = this.getDeclarationDescription(statement);
         const description = this.getDeclarationDescription(statement);
         const docsPage = this.getDocsPage(statement);
         const docsPage = this.getDocsPage(statement);
+        const since = this.getSince(statement);
         const packageName = this.getPackageName(sourceFile);
         const packageName = this.getPackageName(sourceFile);
 
 
         const info = {
         const info = {
@@ -125,6 +126,7 @@ export class TypescriptDocsParser {
             category,
             category,
             description,
             description,
             page: docsPage,
             page: docsPage,
+            since,
         };
         };
 
 
         if (ts.isInterfaceDeclaration(statement)) {
         if (ts.isInterfaceDeclaration(statement)) {
@@ -158,7 +160,7 @@ export class TypescriptDocsParser {
                 members: this.parseMembers(statement.members) as PropertyInfo[],
                 members: this.parseMembers(statement.members) as PropertyInfo[],
             };
             };
         } else if (ts.isFunctionDeclaration(statement)) {
         } else if (ts.isFunctionDeclaration(statement)) {
-            const parameters = statement.parameters.map((p) => ({
+            const parameters = statement.parameters.map(p => ({
                 name: p.name.getText(),
                 name: p.name.getText(),
                 type: p.type ? p.type.getText() : '',
                 type: p.type ? p.type.getText() : '',
                 optional: !!p.questionToken,
                 optional: !!p.questionToken,
@@ -189,7 +191,7 @@ export class TypescriptDocsParser {
         if (!heritageClauses) {
         if (!heritageClauses) {
             return;
             return;
         }
         }
-        const clause = heritageClauses.find((cl) => cl.token === kind);
+        const clause = heritageClauses.find(cl => cl.token === kind);
         if (!clause) {
         if (!clause) {
             return;
             return;
         }
         }
@@ -212,7 +214,7 @@ export class TypescriptDocsParser {
             !ts.isVariableStatement(declaration) &&
             !ts.isVariableStatement(declaration) &&
             declaration.typeParameters
             declaration.typeParameters
         ) {
         ) {
-            typeParams = '<' + declaration.typeParameters.map((tp) => tp.getText()).join(', ') + '>';
+            typeParams = '<' + declaration.typeParameters.map(tp => tp.getText()).join(', ') + '>';
         }
         }
         return name + typeParams;
         return name + typeParams;
     }
     }
@@ -235,7 +237,7 @@ export class TypescriptDocsParser {
         const result: Array<PropertyInfo | MethodInfo> = [];
         const result: Array<PropertyInfo | MethodInfo> = [];
 
 
         for (const member of members) {
         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');
             const isPrivate = modifiers.includes('private');
             if (
             if (
                 !isPrivate &&
                 !isPrivate &&
@@ -259,6 +261,7 @@ export class TypescriptDocsParser {
                 let parameters: MethodParameterInfo[] = [];
                 let parameters: MethodParameterInfo[] = [];
                 let fullText = '';
                 let fullText = '';
                 let isInternal = false;
                 let isInternal = false;
+                let since: string | undefined;
                 if (ts.isConstructorDeclaration(member)) {
                 if (ts.isConstructorDeclaration(member)) {
                     fullText = 'constructor';
                     fullText = 'constructor';
                 } else if (ts.isMethodDeclaration(member)) {
                 } else if (ts.isMethodDeclaration(member)) {
@@ -269,10 +272,11 @@ export class TypescriptDocsParser {
                     fullText = member.getText();
                     fullText = member.getText();
                 }
                 }
                 this.parseTags(member, {
                 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),
+                    since: tag => (since = tag.comment || undefined),
                 });
                 });
                 if (isInternal) {
                 if (isInternal) {
                     continue;
                     continue;
@@ -286,13 +290,14 @@ export class TypescriptDocsParser {
                     description: this.restoreAtTokens(description),
                     description: this.restoreAtTokens(description),
                     type,
                     type,
                     modifiers,
                     modifiers,
+                    since,
                 };
                 };
                 if (
                 if (
                     ts.isMethodSignature(member) ||
                     ts.isMethodSignature(member) ||
                     ts.isMethodDeclaration(member) ||
                     ts.isMethodDeclaration(member) ||
                     ts.isConstructorDeclaration(member)
                     ts.isConstructorDeclaration(member)
                 ) {
                 ) {
-                    parameters = member.parameters.map((p) => ({
+                    parameters = member.parameters.map(p => ({
                         name: p.name.getText(),
                         name: p.name.getText(),
                         type: p.type ? p.type.getText() : '',
                         type: p.type ? p.type.getText() : '',
                         optional: !!p.questionToken,
                         optional: !!p.questionToken,
@@ -322,7 +327,7 @@ export class TypescriptDocsParser {
     private getDeclarationWeight(statement: ValidDeclaration): number {
     private getDeclarationWeight(statement: ValidDeclaration): number {
         let weight = 10;
         let weight = 10;
         this.parseTags(statement, {
         this.parseTags(statement, {
-            docsWeight: (tag) => (weight = Number.parseInt(tag.comment || '10', 10)),
+            docsWeight: tag => (weight = Number.parseInt(tag.comment || '10', 10)),
         });
         });
         return weight;
         return weight;
     }
     }
@@ -330,19 +335,30 @@ export class TypescriptDocsParser {
     private getDocsPage(statement: ValidDeclaration): string | undefined {
     private getDocsPage(statement: ValidDeclaration): string | undefined {
         let docsPage: string | undefined;
         let docsPage: string | undefined;
         this.parseTags(statement, {
         this.parseTags(statement, {
-            docsPage: (tag) => (docsPage = tag.comment),
+            docsPage: tag => (docsPage = tag.comment),
         });
         });
         return docsPage;
         return docsPage;
     }
     }
 
 
+    /**
+     * Reads the @since JSDoc tag
+     */
+    private getSince(statement: ValidDeclaration): string | undefined {
+        let since: string | undefined;
+        this.parseTags(statement, {
+            since: tag => (since = tag.comment),
+        });
+        return since;
+    }
+
     /**
     /**
      * Reads the @description JSDoc tag from the interface.
      * Reads the @description JSDoc tag from the interface.
      */
      */
     private getDeclarationDescription(statement: ValidDeclaration): string {
     private getDeclarationDescription(statement: ValidDeclaration): string {
         let description = '';
         let description = '';
         this.parseTags(statement, {
         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);
         return this.restoreAtTokens(description);
     }
     }
@@ -353,7 +369,7 @@ export class TypescriptDocsParser {
     private getDocsCategory(statement: ValidDeclaration): string | undefined {
     private getDocsCategory(statement: ValidDeclaration): string | undefined {
         let category: string | undefined;
         let category: string | undefined;
         this.parseTags(statement, {
         this.parseTags(statement, {
-            docsCategory: (tag) => (category = tag.comment || ''),
+            docsCategory: tag => (category = tag.comment || ''),
         });
         });
         return this.kebabCase(category);
         return this.kebabCase(category);
     }
     }

+ 10 - 2
scripts/docs/typescript-docs-renderer.ts

@@ -282,6 +282,7 @@ export class TypescriptDocsRenderer {
         let output = '';
         let output = '';
         for (const member of members || []) {
         for (const member of members || []) {
             let defaultParam = '';
             let defaultParam = '';
+            let sinceParam = '';
             let type = '';
             let type = '';
             if (member.kind === 'property') {
             if (member.kind === 'property') {
                 type = this.renderType(member.type, knownTypeMap, docsUrl);
                 type = this.renderType(member.type, knownTypeMap, docsUrl);
@@ -298,10 +299,13 @@ export class TypescriptDocsRenderer {
                     type = `(${args}) => ${this.renderType(member.type, knownTypeMap, docsUrl)}`;
                     type = `(${args}) => ${this.renderType(member.type, knownTypeMap, docsUrl)}`;
                 }
                 }
             }
             }
+            if (member.since) {
+                sinceParam = `since="${member.since}" `;
+            }
             output += `### ${member.name}\n\n`;
             output += `### ${member.name}\n\n`;
             output += `{{< member-info kind="${[...member.modifiers, member.kind].join(
             output += `{{< member-info kind="${[...member.modifiers, member.kind].join(
                 ' ',
                 ' ',
-            )}" type="${type}" ${defaultParam}>}}\n\n`;
+            )}" type="${type}" ${defaultParam} ${sinceParam}>}}\n\n`;
             output += `{{< member-description >}}${this.renderDescription(
             output += `{{< member-description >}}${this.renderDescription(
                 member.description,
                 member.description,
                 knownTypeMap,
                 knownTypeMap,
@@ -326,7 +330,11 @@ export class TypescriptDocsRenderer {
 
 
     private renderGenerationInfoShortcode(info: DeclarationInfo): string {
     private renderGenerationInfoShortcode(info: DeclarationInfo): string {
         const sourceFile = info.sourceFile.replace(/^\.\.\//, '');
         const sourceFile = info.sourceFile.replace(/^\.\.\//, '');
-        return `{{< generation-info sourceFile="${sourceFile}" sourceLine="${info.sourceLine}" packageName="${info.packageName}">}}\n\n`;
+        let sinceData = '';
+        if (info.since) {
+            sinceData = ` since="${info.since}"`;
+        }
+        return `{{< generation-info sourceFile="${sourceFile}" sourceLine="${info.sourceLine}" packageName="${info.packageName}"${sinceData}>}}\n\n`;
     }
     }
 
 
     /**
     /**