diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts index 9383477dc..70383efa4 100644 --- a/lib/plugin/utils/ast-utils.ts +++ b/lib/plugin/utils/ast-utils.ts @@ -18,6 +18,7 @@ import { TypeFormatFlags } from 'typescript'; import { isDynamicallyAdded } from './plugin-utils'; +import { DocComment, DocExcerpt, DocNode, ParserContext, TSDocParser } from '@microsoft/tsdoc'; export function isArray(type: Type) { const symbol = type.getSymbol(); @@ -102,6 +103,30 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) { return formatFlags; } +export function getNodeDocs( + node: Node +): DocComment { + const tsdocParser: TSDocParser = new TSDocParser(); + const parserContext: ParserContext = tsdocParser.parseString(node.getFullText()); + return parserContext.docComment; +} + +export function docNodeToString(docNode: DocNode): string { + let result = ''; + + if (docNode) { + if (docNode instanceof DocExcerpt) { + result += docNode.content.toString(); + } + + for(const childNode of docNode.getChildNodes()) { + result += docNodeToString(childNode); + } + } + + return result.trim(); +} + export function getMainCommentAndExamplesOfNode( node: Node, sourceFile: SourceFile, diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index ebeddcf87..21f9782d6 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -4,8 +4,9 @@ import { ApiOperation, ApiResponse } from '../../decorators'; import { PluginOptions } from '../merge-options'; import { OPENAPI_NAMESPACE } from '../plugin-constants'; import { + docNodeToString, getDecoratorArguments, - getMainCommentAndExamplesOfNode + getMainCommentAndExamplesOfNode, getNodeDocs } from '../utils/ast-utils'; import { getDecoratorOrUndefinedByNames, @@ -116,25 +117,46 @@ export class ControllerClassVisitor extends AbstractFileVisitor { !apiOperationExprProperties || !hasPropertyKey(keyToGenerate, apiOperationExprProperties) ) { - const [extractedComments] = getMainCommentAndExamplesOfNode( - node, - sourceFile, - typeChecker - ); - if (!extractedComments) { - // Node does not have any comments - return []; + const properties = []; + + if (keyToGenerate) { + const [extractedComments] = getMainCommentAndExamplesOfNode( + node, + sourceFile, + typeChecker + ); + + if (!extractedComments) { + // Node does not have any comments + return []; + } + + properties.push(ts.createPropertyAssignment(keyToGenerate, ts.createLiteral(extractedComments))); + } else { + const docs = getNodeDocs(node); + + if (!docs) { + return []; + } + + const summary = docNodeToString(docs.summarySection); + if (summary && (!apiOperationExprProperties || !hasPropertyKey("summary", apiOperationExprProperties))) { + properties.push(ts.createPropertyAssignment("summary", ts.createLiteral(summary))); + } + + const remarks = docNodeToString(docs.remarksBlock.content); + if (remarks && (!apiOperationExprProperties || !hasPropertyKey("description", apiOperationExprProperties))) { + properties.push(ts.createPropertyAssignment("description", ts.createLiteral(remarks))); + } } - const properties = [ - ts.createPropertyAssignment( - keyToGenerate, - ts.createLiteral(extractedComments) - ), - ...(apiOperationExprProperties ?? ts.createNodeArray()) - ]; + const apiOperationDecoratorArguments: ts.NodeArray = ts.createNodeArray( - [ts.createObjectLiteral(compact(properties))] + [ts.createObjectLiteral(compact([ + ...properties, + ...(apiOperationExprProperties ?? ts.createNodeArray()) + ]))] ); + if (apiOperationDecorator) { ((apiOperationDecorator.expression as ts.CallExpression) as any).arguments = apiOperationDecoratorArguments; } else { diff --git a/package-lock.json b/package-lock.json index c9e396e33..b087d6879 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "4.7.12", "license": "MIT", "dependencies": { + "@microsoft/tsdoc": "^0.13.0", "@nestjs/mapped-types": "0.3.0", "lodash": "4.17.20", "path-to-regexp": "3.2.0" @@ -1825,6 +1826,7 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -2317,6 +2319,11 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "node_modules/@microsoft/tsdoc": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.0.tgz", + "integrity": "sha512-/8J+4DdvexBH1Qh1yR8VZ6bPay2DL/TDdmSIypAa3dAghJzsdaiZG8COvzpYIML6HV2UVN0g4qbuqzjG4YKgWg==" + }, "node_modules/@nestjs/common": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-7.6.12.tgz", @@ -5400,7 +5407,8 @@ "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", - "optionator": "^0.8.1" + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -9332,6 +9340,7 @@ "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", @@ -11552,6 +11561,7 @@ "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", "dev": true, "dependencies": { + "graceful-fs": "^4.1.6", "universalify": "^1.0.0" }, "optionalDependencies": { @@ -17325,6 +17335,7 @@ "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==", "dev": true, "dependencies": { + "commander": "^2.7.1", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^12.0.0" @@ -19136,6 +19147,11 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "@microsoft/tsdoc": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.0.tgz", + "integrity": "sha512-/8J+4DdvexBH1Qh1yR8VZ6bPay2DL/TDdmSIypAa3dAghJzsdaiZG8COvzpYIML6HV2UVN0g4qbuqzjG4YKgWg==" + }, "@nestjs/common": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-7.6.12.tgz", diff --git a/package.json b/package.json index c42c80f3f..b74b6aaf6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "release": "release-it" }, "dependencies": { + "@microsoft/tsdoc": "^0.13.0", "@nestjs/mapped-types": "0.3.0", "lodash": "4.17.20", "path-to-regexp": "3.2.0" diff --git a/test/plugin/controller-class-visitor.spec.ts b/test/plugin/controller-class-visitor.spec.ts index 0e0bc9bf6..a376f6c0a 100644 --- a/test/plugin/controller-class-visitor.spec.ts +++ b/test/plugin/controller-class-visitor.spec.ts @@ -4,30 +4,54 @@ import { appControllerText, appControllerTextTranspiled } from './fixtures/app.controller'; +import { + enhancedCommentsControllerText, + enhancedCommentsControllerTextTranspiled +} from './fixtures/enhanced-comments.controller'; + +const compilerOptions: ts.CompilerOptions = { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ESNext, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true +} + +const transpileModule = (filename, controllerText, compilerOptions, swaggerDocumentOptions = {}) => { + const fakeProgram = ts.createProgram([filename], compilerOptions); + + return ts.transpileModule(controllerText, { + compilerOptions, + fileName: filename, + transformers: { + before: [ + before( + {...swaggerDocumentOptions, introspectComments: true }, + fakeProgram + ) + ] + } + }) +} describe('Controller methods', () => { - it('should add response based on the return value', () => { - const options: ts.CompilerOptions = { - module: ts.ModuleKind.CommonJS, - target: ts.ScriptTarget.ESNext, - newLine: ts.NewLineKind.LineFeed, - noEmitHelpers: true - }; - const filename = 'app.controller.ts'; - const fakeProgram = ts.createProgram([filename], options); + it('Should generate summary property', () => { + const result = transpileModule( + 'app.controller.ts', + appControllerText, + compilerOptions, + {controllerKeyOfComment: 'summary'} + ); - const result = ts.transpileModule(appControllerText, { - compilerOptions: options, - fileName: filename, - transformers: { - before: [ - before( - { controllerKeyOfComment: 'summary', introspectComments: true }, - fakeProgram - ) - ] - } - }); expect(result.outputText).toEqual(appControllerTextTranspiled); }); + + it('Should generate summary and description if no controllerKeyOfComments', () => { + const result = transpileModule( + 'enhanced-comments.controller.ts', + enhancedCommentsControllerText, + compilerOptions, + { controllerKeyOfComment: null } + ); + expect(result.outputText).toEqual(enhancedCommentsControllerTextTranspiled); + }) }); diff --git a/test/plugin/fixtures/enhanced-comments.controller.ts b/test/plugin/fixtures/enhanced-comments.controller.ts new file mode 100644 index 000000000..b775b9c09 --- /dev/null +++ b/test/plugin/fixtures/enhanced-comments.controller.ts @@ -0,0 +1,109 @@ +export const enhancedCommentsControllerText = `import { Controller, Post, HttpStatus } from '@nestjs/common'; +import { ApiOperation } from '@nestjs/swagger'; + +class Cat {} + +@Controller('cats') +export class EnhancedCommentsController { + onApplicationBootstrap() {} + + /** + * create a Cat + * + * @remarks + * Create a super nice cat + * + * @returns {Promise} + * @memberof AppController + */ + @Post() + async create(): Promise {} + + /** + * find a Cat + * + * @remarks + * Find the best cat in the world + */ + @ApiOperation({}) + @Get() + async findOne(): Promise {} + + /** + * find all Cats im comment + * + * @remarks + * Find all cats while you write comments + * + * @returns {Promise} + * @memberof AppController + */ + @ApiOperation({ + summary: 'find all Cats', + description: 'Find all cats while you write decorators' + }) + @Get() + @HttpCode(HttpStatus.NO_CONTENT) + async findAll(): Promise {} +}`; + +export const enhancedCommentsControllerTextTranspiled = `"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EnhancedCommentsController = void 0; +const openapi = require("@nestjs/swagger"); +const common_1 = require("@nestjs/common"); +const swagger_1 = require("@nestjs/swagger"); +class Cat { +} +let EnhancedCommentsController = class EnhancedCommentsController { + onApplicationBootstrap() { } + /** + * create a Cat + * + * @remarks + * Create a super nice cat + * + * @returns {Promise} + * @memberof AppController + */ + async create() { } + /** + * find a Cat + * + * @remarks + * Find the best cat in the world + */ + async findOne() { } + /** + * find all Cats im comment + * + * @remarks + * Find all cats while you write comments + * + * @returns {Promise} + * @memberof AppController + */ + async findAll() { } +}; +__decorate([ + openapi.ApiOperation({ summary: "create a Cat", description: "Create a super nice cat" }), + common_1.Post(), + openapi.ApiResponse({ status: 201, type: Cat }) +], EnhancedCommentsController.prototype, "create", null); +__decorate([ + swagger_1.ApiOperation({ summary: "find a Cat", description: "Find the best cat in the world" }), + Get(), + openapi.ApiResponse({ status: 200, type: Cat }) +], EnhancedCommentsController.prototype, "findOne", null); +__decorate([ + swagger_1.ApiOperation({ summary: 'find all Cats', + description: 'Find all cats while you write decorators' }), + Get(), + HttpCode(common_1.HttpStatus.NO_CONTENT), + openapi.ApiResponse({ status: common_1.HttpStatus.NO_CONTENT, type: [Cat] }) +], EnhancedCommentsController.prototype, "findAll", null); +EnhancedCommentsController = __decorate([ + common_1.Controller('cats') +], EnhancedCommentsController); +exports.EnhancedCommentsController = EnhancedCommentsController; +`;