Skip to content

Commit 90447b1

Browse files
committedFeb 7, 2024
Merge branch 'features/enhanced-controller-comments' into features/enhanced-comments
2 parents 500a718 + dc48ef1 commit 90447b1

File tree

6 files changed

+8352
-223
lines changed

6 files changed

+8352
-223
lines changed
 

‎lib/plugin/utils/ast-utils.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
UnionTypeNode
2222
} from 'typescript';
2323
import { isDynamicallyAdded } from './plugin-utils';
24+
import { DocComment, DocExcerpt, DocNode, ParserContext, TSDocParser } from '@microsoft/tsdoc';
2425

2526
export function isArray(type: Type) {
2627
const symbol = type.getSymbol();
@@ -117,7 +118,31 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
117118
return formatFlags;
118119
}
119120

120-
export function getMainCommentOfNode(
121+
export function getNodeDocs(
122+
node: Node
123+
): DocComment {
124+
const tsdocParser: TSDocParser = new TSDocParser();
125+
const parserContext: ParserContext = tsdocParser.parseString(node.getFullText());
126+
return parserContext.docComment;
127+
}
128+
129+
export function docNodeToString(docNode: DocNode): string {
130+
let result = '';
131+
132+
if (docNode) {
133+
if (docNode instanceof DocExcerpt) {
134+
result += docNode.content.toString();
135+
}
136+
137+
for(const childNode of docNode.getChildNodes()) {
138+
result += docNodeToString(childNode);
139+
}
140+
}
141+
142+
return result.trim();
143+
}
144+
145+
export function getMainCommentAndExamplesOfNode(
121146
node: Node,
122147
sourceFile: SourceFile
123148
): string {

‎lib/plugin/visitors/controller-class.visitor.ts

+58-76
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import { ApiOperation, ApiResponse } from '../../decorators';
55
import { PluginOptions } from '../merge-options';
66
import { OPENAPI_NAMESPACE } from '../plugin-constants';
77
import {
8-
createLiteralFromAnyValue,
8+
docNodeToString,
99
getDecoratorArguments,
10-
getDecoratorName,
11-
getMainCommentOfNode,
12-
getTsDocTagsOfNode
10+
getMainCommentAndExamplesOfNode, getNodeDocs
1311
} from '../utils/ast-utils';
1412
import {
1513
convertPath,
@@ -205,85 +203,69 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
205203
decorators,
206204
factory
207205
);
208-
let apiOperationExistingProps:
209-
| ts.NodeArray<ts.PropertyAssignment>
210-
| undefined = undefined;
206+
const apiOperationExpr: ts.ObjectLiteralExpression | undefined =
207+
apiOperationDecorator &&
208+
head(getDecoratorArguments(apiOperationDecorator));
209+
const apiOperationExprProperties =
210+
apiOperationExpr &&
211+
(apiOperationExpr.properties as ts.NodeArray<ts.PropertyAssignment>);
211212

212-
if (apiOperationDecorator && !options.readonly) {
213-
const apiOperationExpr = head(
214-
getDecoratorArguments(apiOperationDecorator)
215-
);
216-
if (apiOperationExpr) {
217-
apiOperationExistingProps =
218-
apiOperationExpr.properties as ts.NodeArray<ts.PropertyAssignment>;
219-
}
220-
}
213+
if (
214+
!apiOperationDecorator ||
215+
!apiOperationExpr ||
216+
!apiOperationExprProperties ||
217+
!hasPropertyKey(keyToGenerate, apiOperationExprProperties)
218+
) {
219+
const properties = [];
221220

222-
const extractedComments = getMainCommentOfNode(node, sourceFile);
223-
if (!extractedComments) {
224-
return [];
225-
}
226-
const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker);
227-
228-
const properties = [
229-
factory.createPropertyAssignment(
230-
keyToGenerate,
231-
factory.createStringLiteral(extractedComments)
232-
),
233-
...(apiOperationExistingProps ?? factory.createNodeArray())
234-
];
221+
if (keyToGenerate) {
222+
const [extractedComments] = getMainCommentAndExamplesOfNode(
223+
node,
224+
sourceFile,
225+
typeChecker
226+
);
235227

236-
const hasDeprecatedKey = hasPropertyKey(
237-
'deprecated',
238-
factory.createNodeArray(apiOperationExistingProps)
239-
);
240-
if (!hasDeprecatedKey && tags.deprecated) {
241-
const deprecatedPropertyAssignment = factory.createPropertyAssignment(
242-
'deprecated',
243-
createLiteralFromAnyValue(factory, tags.deprecated)
244-
);
245-
properties.push(deprecatedPropertyAssignment);
246-
}
228+
if (!extractedComments) {
229+
// Node does not have any comments
230+
return [];
231+
}
247232

248-
const objectLiteralExpr = factory.createObjectLiteralExpression(
249-
compact(properties)
250-
);
251-
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> =
252-
factory.createNodeArray([objectLiteralExpr]);
233+
properties.push(ts.createPropertyAssignment(keyToGenerate, ts.createLiteral(extractedComments)));
234+
} else {
235+
const docs = getNodeDocs(node);
253236

254-
const methodKey = node.name.getText();
255-
if (metadata[methodKey]) {
256-
const existingObjectLiteralExpr = metadata[methodKey];
257-
const existingProperties = existingObjectLiteralExpr.properties;
258-
const updatedProperties = factory.createNodeArray([
259-
...existingProperties,
260-
...compact(properties)
261-
]);
262-
const updatedObjectLiteralExpr =
263-
factory.createObjectLiteralExpression(updatedProperties);
264-
metadata[methodKey] = updatedObjectLiteralExpr;
265-
} else {
266-
metadata[methodKey] = objectLiteralExpr;
267-
}
237+
if (!docs) {
238+
return [];
239+
}
268240

269-
if (apiOperationDecorator) {
270-
const expr = apiOperationDecorator.expression as any as ts.CallExpression;
271-
const updatedCallExpr = factory.updateCallExpression(
272-
expr,
273-
expr.expression,
274-
undefined,
275-
apiOperationDecoratorArguments
241+
const summary = docNodeToString(docs.summarySection);
242+
if (summary && (!apiOperationExprProperties || !hasPropertyKey("summary", apiOperationExprProperties))) {
243+
properties.push(ts.createPropertyAssignment("summary", ts.createLiteral(summary)));
244+
}
245+
246+
const remarks = docNodeToString(docs.remarksBlock.content);
247+
if (remarks && (!apiOperationExprProperties || !hasPropertyKey("description", apiOperationExprProperties))) {
248+
properties.push(ts.createPropertyAssignment("description", ts.createLiteral(remarks)));
249+
}
250+
}
251+
252+
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> = ts.createNodeArray(
253+
[ts.createObjectLiteral(compact([
254+
...properties,
255+
...(apiOperationExprProperties ?? ts.createNodeArray())
256+
]))]
276257
);
277-
return [factory.updateDecorator(apiOperationDecorator, updatedCallExpr)];
278-
} else {
279-
return [
280-
factory.createDecorator(
281-
factory.createCallExpression(
282-
factory.createIdentifier(
283-
`${OPENAPI_NAMESPACE}.${ApiOperation.name}`
284-
),
285-
undefined,
286-
apiOperationDecoratorArguments
258+
259+
if (apiOperationDecorator) {
260+
((apiOperationDecorator.expression as ts.CallExpression) as any).arguments = apiOperationDecoratorArguments;
261+
} else {
262+
return [
263+
ts.createDecorator(
264+
ts.createCall(
265+
ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiOperation.name}`),
266+
undefined,
267+
apiOperationDecoratorArguments
268+
)
287269
)
288270
)
289271
];

‎package-lock.json

+8,113-60
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
"start:debug": "nest start --watch --debug"
2525
},
2626
"dependencies": {
27-
"@nestjs/mapped-types": "2.0.4",
28-
"js-yaml": "4.1.0",
29-
"lodash": "4.17.21",
30-
"path-to-regexp": "3.2.0",
31-
"swagger-ui-dist": "5.11.0"
27+
"@microsoft/tsdoc": "^0.13.0",
28+
"@nestjs/mapped-types": "0.3.0",
29+
"lodash": "4.17.20",
30+
"path-to-regexp": "3.2.0"
3231
},
3332
"devDependencies": {
3433
"@commitlint/cli": "18.6.0",

‎test/plugin/controller-class-visitor.spec.ts

+42-81
Original file line numberDiff line numberDiff line change
@@ -5,92 +5,53 @@ import {
55
appControllerTextTranspiled
66
} from './fixtures/app.controller';
77
import {
8-
appControllerWithTabsText,
9-
appControllerWithTabsTextTranspiled
10-
} from './fixtures/app.controller-tabs';
11-
import {
12-
appControllerWithoutModifiersText,
13-
appControllerWithoutModifiersTextTranspiled
14-
} from './fixtures/app.controller-without-modifiers';
8+
enhancedCommentsControllerText,
9+
enhancedCommentsControllerTextTranspiled
10+
} from './fixtures/enhanced-comments.controller';
1511

16-
describe('Controller methods', () => {
17-
it('should add response based on the return value (spaces)', () => {
18-
const options: ts.CompilerOptions = {
19-
module: ts.ModuleKind.CommonJS,
20-
target: ts.ScriptTarget.ES2021,
21-
newLine: ts.NewLineKind.LineFeed,
22-
noEmitHelpers: true,
23-
experimentalDecorators: true
24-
};
25-
const filename = 'app.controller.ts';
26-
const fakeProgram = ts.createProgram([filename], options);
12+
const compilerOptions: ts.CompilerOptions = {
13+
module: ts.ModuleKind.CommonJS,
14+
target: ts.ScriptTarget.ESNext,
15+
newLine: ts.NewLineKind.LineFeed,
16+
noEmitHelpers: true
17+
}
2718

28-
const result = ts.transpileModule(appControllerText, {
29-
compilerOptions: options,
30-
fileName: filename,
31-
transformers: {
32-
before: [
33-
before(
34-
{ controllerKeyOfComment: 'summary', introspectComments: true },
35-
fakeProgram
36-
)
37-
]
38-
}
39-
});
40-
expect(result.outputText).toEqual(appControllerTextTranspiled);
41-
});
19+
const transpileModule = (filename, controllerText, compilerOptions, swaggerDocumentOptions = {}) => {
20+
const fakeProgram = ts.createProgram([filename], compilerOptions);
4221

43-
it('should add response based on the return value (tabs)', () => {
44-
const options: ts.CompilerOptions = {
45-
module: ts.ModuleKind.CommonJS,
46-
target: ts.ScriptTarget.ES2021,
47-
newLine: ts.NewLineKind.LineFeed,
48-
noEmitHelpers: true,
49-
experimentalDecorators: true
50-
};
51-
const filename = 'app.controller.ts';
52-
const fakeProgram = ts.createProgram([filename], options);
22+
return ts.transpileModule(controllerText, {
23+
compilerOptions,
24+
fileName: filename,
25+
transformers: {
26+
before: [
27+
before(
28+
{...swaggerDocumentOptions, introspectComments: true },
29+
fakeProgram
30+
)
31+
]
32+
}
33+
})
34+
}
5335

54-
const result = ts.transpileModule(appControllerWithTabsText, {
55-
compilerOptions: options,
56-
fileName: filename,
57-
transformers: {
58-
before: [
59-
before(
60-
{ controllerKeyOfComment: 'summary', introspectComments: true },
61-
fakeProgram
62-
)
63-
]
64-
}
65-
});
66-
expect(result.outputText).toEqual(appControllerWithTabsTextTranspiled);
67-
});
36+
describe('Controller methods', () => {
37+
it('Should generate summary property', () => {
38+
const result = transpileModule(
39+
'app.controller.ts',
40+
appControllerText,
41+
compilerOptions,
42+
{controllerKeyOfComment: 'summary'}
43+
);
6844

69-
it('should add response based on the return value (without modifiers)', () => {
70-
const options: ts.CompilerOptions = {
71-
module: ts.ModuleKind.CommonJS,
72-
target: ts.ScriptTarget.ES2021,
73-
newLine: ts.NewLineKind.LineFeed,
74-
noEmitHelpers: true,
75-
experimentalDecorators: true
76-
};
77-
const filename = 'app.controller.ts';
78-
const fakeProgram = ts.createProgram([filename], options);
45+
expect(result.outputText).toEqual(appControllerTextTranspiled);
46+
});
7947

80-
const result = ts.transpileModule(appControllerWithoutModifiersText, {
81-
compilerOptions: options,
82-
fileName: filename,
83-
transformers: {
84-
before: [
85-
before(
86-
{ controllerKeyOfComment: 'summary', introspectComments: true },
87-
fakeProgram
88-
)
89-
]
90-
}
91-
});
92-
expect(result.outputText).toEqual(
93-
appControllerWithoutModifiersTextTranspiled
48+
it('Should generate summary and description if no controllerKeyOfComments', () => {
49+
const result = transpileModule(
50+
'enhanced-comments.controller.ts',
51+
enhancedCommentsControllerText,
52+
compilerOptions,
53+
{ controllerKeyOfComment: null }
9454
);
95-
});
55+
expect(result.outputText).toEqual(enhancedCommentsControllerTextTranspiled);
56+
})
9657
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
export const enhancedCommentsControllerText = `import { Controller, Post, HttpStatus } from '@nestjs/common';
2+
import { ApiOperation } from '@nestjs/swagger';
3+
4+
class Cat {}
5+
6+
@Controller('cats')
7+
export class EnhancedCommentsController {
8+
onApplicationBootstrap() {}
9+
10+
/**
11+
* create a Cat
12+
*
13+
* @remarks
14+
* Create a super nice cat
15+
*
16+
* @returns {Promise<Cat>}
17+
* @memberof AppController
18+
*/
19+
@Post()
20+
async create(): Promise<Cat> {}
21+
22+
/**
23+
* find a Cat
24+
*
25+
* @remarks
26+
* Find the best cat in the world
27+
*/
28+
@ApiOperation({})
29+
@Get()
30+
async findOne(): Promise<Cat> {}
31+
32+
/**
33+
* find all Cats im comment
34+
*
35+
* @remarks
36+
* Find all cats while you write comments
37+
*
38+
* @returns {Promise<Cat>}
39+
* @memberof AppController
40+
*/
41+
@ApiOperation({
42+
summary: 'find all Cats',
43+
description: 'Find all cats while you write decorators'
44+
})
45+
@Get()
46+
@HttpCode(HttpStatus.NO_CONTENT)
47+
async findAll(): Promise<Cat[]> {}
48+
}`;
49+
50+
export const enhancedCommentsControllerTextTranspiled = `"use strict";
51+
Object.defineProperty(exports, "__esModule", { value: true });
52+
exports.EnhancedCommentsController = void 0;
53+
const openapi = require("@nestjs/swagger");
54+
const common_1 = require("@nestjs/common");
55+
const swagger_1 = require("@nestjs/swagger");
56+
class Cat {
57+
}
58+
let EnhancedCommentsController = class EnhancedCommentsController {
59+
onApplicationBootstrap() { }
60+
/**
61+
* create a Cat
62+
*
63+
* @remarks
64+
* Create a super nice cat
65+
*
66+
* @returns {Promise<Cat>}
67+
* @memberof AppController
68+
*/
69+
async create() { }
70+
/**
71+
* find a Cat
72+
*
73+
* @remarks
74+
* Find the best cat in the world
75+
*/
76+
async findOne() { }
77+
/**
78+
* find all Cats im comment
79+
*
80+
* @remarks
81+
* Find all cats while you write comments
82+
*
83+
* @returns {Promise<Cat>}
84+
* @memberof AppController
85+
*/
86+
async findAll() { }
87+
};
88+
__decorate([
89+
openapi.ApiOperation({ summary: "create a Cat", description: "Create a super nice cat" }),
90+
common_1.Post(),
91+
openapi.ApiResponse({ status: 201, type: Cat })
92+
], EnhancedCommentsController.prototype, "create", null);
93+
__decorate([
94+
swagger_1.ApiOperation({ summary: "find a Cat", description: "Find the best cat in the world" }),
95+
Get(),
96+
openapi.ApiResponse({ status: 200, type: Cat })
97+
], EnhancedCommentsController.prototype, "findOne", null);
98+
__decorate([
99+
swagger_1.ApiOperation({ summary: 'find all Cats',
100+
description: 'Find all cats while you write decorators' }),
101+
Get(),
102+
HttpCode(common_1.HttpStatus.NO_CONTENT),
103+
openapi.ApiResponse({ status: common_1.HttpStatus.NO_CONTENT, type: [Cat] })
104+
], EnhancedCommentsController.prototype, "findAll", null);
105+
EnhancedCommentsController = __decorate([
106+
common_1.Controller('cats')
107+
], EnhancedCommentsController);
108+
exports.EnhancedCommentsController = EnhancedCommentsController;
109+
`;

0 commit comments

Comments
 (0)
Please sign in to comment.