Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(introspectComments): Enhanced introduction comments using tsdocs #1207

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
56 changes: 39 additions & 17 deletions lib/plugin/visitors/controller-class.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.Expression> = 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 {
Expand Down
18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
66 changes: 45 additions & 21 deletions test/plugin/controller-class-visitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
});
109 changes: 109 additions & 0 deletions test/plugin/fixtures/enhanced-comments.controller.ts
Original file line number Diff line number Diff line change
@@ -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<Cat>}
* @memberof AppController
*/
@Post()
async create(): Promise<Cat> {}

/**
* find a Cat
*
* @remarks
* Find the best cat in the world
*/
@ApiOperation({})
@Get()
async findOne(): Promise<Cat> {}

/**
* find all Cats im comment
*
* @remarks
* Find all cats while you write comments
*
* @returns {Promise<Cat>}
* @memberof AppController
*/
@ApiOperation({
summary: 'find all Cats',
description: 'Find all cats while you write decorators'
})
@Get()
@HttpCode(HttpStatus.NO_CONTENT)
async findAll(): Promise<Cat[]> {}
}`;

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<Cat>}
* @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<Cat>}
* @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;
`;