Skip to content

Commit d3e7e1c

Browse files
committedFeb 7, 2024
feat: Update code for latest package
1 parent 90447b1 commit d3e7e1c

7 files changed

+326
-8441
lines changed
 

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

+89-111
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,37 @@ import {
2121
UnionTypeNode
2222
} from 'typescript';
2323
import { isDynamicallyAdded } from './plugin-utils';
24-
import { DocComment, DocExcerpt, DocNode, ParserContext, TSDocParser } from '@microsoft/tsdoc';
24+
import {
25+
DocNode,
26+
DocExcerpt,
27+
TSDocParser,
28+
ParserContext,
29+
DocComment,
30+
DocBlock
31+
} from '@microsoft/tsdoc';
32+
33+
export class Formatter {
34+
public static renderDocNode(docNode: DocNode): string {
35+
let result: string = '';
36+
if (docNode) {
37+
if (docNode instanceof DocExcerpt) {
38+
result += docNode.content.toString();
39+
}
40+
for (const childNode of docNode.getChildNodes()) {
41+
result += Formatter.renderDocNode(childNode);
42+
}
43+
}
44+
return result;
45+
}
46+
47+
public static renderDocNodes(docNodes: ReadonlyArray<DocNode>): string {
48+
let result: string = '';
49+
for (const docNode of docNodes) {
50+
result += Formatter.renderDocNode(docNode);
51+
}
52+
return result;
53+
}
54+
}
2555

2656
export function isArray(type: Type) {
2757
const symbol = type.getSymbol();
@@ -118,142 +148,90 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
118148
return formatFlags;
119149
}
120150

121-
export function getNodeDocs(
122-
node: Node
123-
): DocComment {
151+
export function getMainCommentOfNode(
152+
node: Node,
153+
sourceFile: SourceFile
154+
): string {
124155
const tsdocParser: TSDocParser = new TSDocParser();
125-
const parserContext: ParserContext = tsdocParser.parseString(node.getFullText());
126-
return parserContext.docComment;
156+
const parserContext: ParserContext = tsdocParser.parseString(
157+
node.getFullText()
158+
);
159+
const docComment: DocComment = parserContext.docComment;
160+
return Formatter.renderDocNode(docComment.summarySection).trim();
127161
}
128162

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);
163+
export function parseCommentDocValue(docValue: string, type: ts.Type) {
164+
let value = docValue.replace(/'/g, '"').trim();
165+
166+
if (!type || !isString(type)) {
167+
try {
168+
value = JSON.parse(value);
169+
} catch {}
170+
} else if (isString(type)) {
171+
if (value.split(' ').length !== 1 && !value.startsWith('"')) {
172+
value = null;
173+
} else {
174+
value = value.replace(/"/g, '');
139175
}
140176
}
141-
142-
return result.trim();
177+
return value;
143178
}
144179

145-
export function getMainCommentAndExamplesOfNode(
146-
node: Node,
147-
sourceFile: SourceFile
148-
): string {
149-
const sourceText = sourceFile.getFullText();
150-
// in case we decide to include "// comments"
151-
const replaceRegex =
152-
/^\s*\** *@.*$|^\s*\/\*+ *|^\s*\/\/+.*|^\s*\/+ *|^\s*\*+ *| +$| *\**\/ *$/gim;
153-
//const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
154-
155-
const commentResult = [];
156-
const introspectComments = (comments?: CommentRange[]) =>
157-
comments?.forEach((comment) => {
158-
const commentSource = sourceText.substring(comment.pos, comment.end);
159-
const oneComment = commentSource.replace(replaceRegex, '').trim();
160-
if (oneComment) {
161-
commentResult.push(oneComment);
162-
}
163-
});
164-
165-
const leadingCommentRanges = getLeadingCommentRanges(
166-
sourceText,
167-
node.getFullStart()
180+
export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) {
181+
const tsdocParser: TSDocParser = new TSDocParser();
182+
const parserContext: ParserContext = tsdocParser.parseString(
183+
node.getFullText()
168184
);
169-
introspectComments(leadingCommentRanges);
170-
if (!commentResult.length) {
171-
const trailingCommentRanges = getTrailingCommentRanges(
172-
sourceText,
173-
node.getFullStart()
174-
);
175-
introspectComments(trailingCommentRanges);
176-
}
177-
return commentResult.join('\n');
178-
}
179-
180-
export function getTsDocTagsOfNode(
181-
node: Node,
182-
sourceFile: SourceFile,
183-
typeChecker: TypeChecker
184-
) {
185-
const sourceText = sourceFile.getFullText();
185+
const docComment: DocComment = parserContext.docComment;
186186

187187
const tagDefinitions: {
188188
[key: string]: {
189-
regex: RegExp;
190189
hasProperties: boolean;
191190
repeatable: boolean;
192191
};
193192
} = {
194193
example: {
195-
regex:
196-
/@example *((['"](?<string>.+?)['"])|(?<booleanOrNumber>[^ ]+?)|(?<array>(\[.+?\]))) *$/gim,
197194
hasProperties: true,
198195
repeatable: true
199-
},
200-
deprecated: {
201-
regex: /@deprecated */gim,
202-
hasProperties: false,
203-
repeatable: false
204196
}
205197
};
206198

207199
const tagResults: any = {};
208-
const introspectTsDocTags = (comments?: CommentRange[]) =>
209-
comments?.forEach((comment) => {
210-
const commentSource = sourceText.substring(comment.pos, comment.end);
211-
212-
for (const tag in tagDefinitions) {
213-
const { regex, hasProperties, repeatable } = tagDefinitions[tag];
214-
215-
let value: any;
216-
217-
let execResult: RegExpExecArray;
218-
while (
219-
(execResult = regex.exec(commentSource)) &&
220-
(!hasProperties || execResult.length > 1)
221-
) {
222-
if (repeatable && !tagResults[tag]) tagResults[tag] = [];
223-
224-
if (hasProperties) {
225-
const docValue =
226-
execResult.groups?.string ??
227-
execResult.groups?.booleanOrNumber ??
228-
(execResult.groups?.array &&
229-
execResult.groups.array.replace(/'/g, '"'));
230-
231-
const type = typeChecker.getTypeAtLocation(node);
232-
233-
value = docValue;
234-
if (!type || !isString(type)) {
235-
try {
236-
value = JSON.parse(value);
237-
} catch {}
238-
}
239-
} else {
240-
value = true;
241-
}
242200

243-
if (repeatable) {
244-
tagResults[tag].push(value);
245-
} else {
246-
tagResults[tag] = value;
201+
const introspectTsDocTags = (docComment: DocComment) => {
202+
for (const tag in tagDefinitions) {
203+
const { hasProperties, repeatable } = tagDefinitions[tag];
204+
const blocks = docComment.customBlocks.filter(
205+
(block) => block.blockTag.tagName === `@${tag}`
206+
);
207+
if (blocks.length === 0) continue;
208+
if (repeatable && !tagResults[tag]) tagResults[tag] = [];
209+
const type = typeChecker.getTypeAtLocation(node);
210+
if (hasProperties) {
211+
blocks.forEach((block) => {
212+
const docValue = Formatter.renderDocNode(block.content).split(
213+
'\n'
214+
)[0];
215+
const value = parseCommentDocValue(docValue, type);
216+
217+
if (value !== null) {
218+
if (repeatable) {
219+
tagResults[tag].push(value);
220+
} else {
221+
tagResults[tag] = value;
222+
}
247223
}
248-
}
224+
});
225+
} else {
226+
tagResults[tag] = true;
249227
}
250-
});
228+
}
229+
if (docComment.deprecatedBlock) {
230+
tagResults['deprecated'] = true;
231+
}
232+
};
233+
introspectTsDocTags(docComment);
251234

252-
const leadingCommentRanges = getLeadingCommentRanges(
253-
sourceText,
254-
node.getFullStart()
255-
);
256-
introspectTsDocTags(leadingCommentRanges);
257235
return tagResults;
258236
}
259237

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

+76-58
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { ApiOperation, ApiResponse } from '../../decorators';
55
import { PluginOptions } from '../merge-options';
66
import { OPENAPI_NAMESPACE } from '../plugin-constants';
77
import {
8-
docNodeToString,
8+
createLiteralFromAnyValue,
99
getDecoratorArguments,
10-
getMainCommentAndExamplesOfNode, getNodeDocs
10+
getDecoratorName,
11+
getMainCommentOfNode,
12+
getTsDocTagsOfNode
1113
} from '../utils/ast-utils';
1214
import {
1315
convertPath,
@@ -203,69 +205,85 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
203205
decorators,
204206
factory
205207
);
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>);
208+
let apiOperationExistingProps:
209+
| ts.NodeArray<ts.PropertyAssignment>
210+
| undefined = undefined;
212211

213-
if (
214-
!apiOperationDecorator ||
215-
!apiOperationExpr ||
216-
!apiOperationExprProperties ||
217-
!hasPropertyKey(keyToGenerate, apiOperationExprProperties)
218-
) {
219-
const properties = [];
220-
221-
if (keyToGenerate) {
222-
const [extractedComments] = getMainCommentAndExamplesOfNode(
223-
node,
224-
sourceFile,
225-
typeChecker
226-
);
227-
228-
if (!extractedComments) {
229-
// Node does not have any comments
230-
return [];
231-
}
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+
}
232221

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

237-
if (!docs) {
238-
return [];
239-
}
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+
}
240247

241-
const summary = docNodeToString(docs.summarySection);
242-
if (summary && (!apiOperationExprProperties || !hasPropertyKey("summary", apiOperationExprProperties))) {
243-
properties.push(ts.createPropertyAssignment("summary", ts.createLiteral(summary)));
244-
}
248+
const objectLiteralExpr = factory.createObjectLiteralExpression(
249+
compact(properties)
250+
);
251+
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> =
252+
factory.createNodeArray([objectLiteralExpr]);
245253

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-
}
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+
}
251268

252-
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> = ts.createNodeArray(
253-
[ts.createObjectLiteral(compact([
254-
...properties,
255-
...(apiOperationExprProperties ?? ts.createNodeArray())
256-
]))]
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
257276
);
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-
)
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
269287
)
270288
)
271289
];

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

+5-6
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
695695
return result;
696696
}
697697

698-
const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
698+
const clonedMinLength =
699+
this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
699700
if (clonedMinLength) {
700701
result.push(
701702
factory.createPropertyAssignment('minLength', clonedMinLength)
@@ -707,10 +708,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
707708
if (!canReferenceNode(maxLength, options)) {
708709
return result;
709710
}
710-
const clonedMaxLength = this.clonePrimitiveLiteral(
711-
factory,
712-
maxLength
713-
) ?? maxLength;
711+
const clonedMaxLength =
712+
this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength;
714713
if (clonedMaxLength) {
715714
result.push(
716715
factory.createPropertyAssignment('maxLength', clonedMaxLength)
@@ -822,7 +821,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
822821
}
823822
const propertyAssignments = [];
824823
const comments = getMainCommentOfNode(node, sourceFile);
825-
const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker);
824+
const tags = getTsDocTagsOfNode(node, typeChecker);
826825

827826
const keyOfComment = options.dtoKeyOfComment;
828827
if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {

‎package-lock.json

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

‎package.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
"start:debug": "nest start --watch --debug"
2525
},
2626
"dependencies": {
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"
27+
"@microsoft/tsdoc": "^0.14.2",
28+
"@nestjs/mapped-types": "2.0.4",
29+
"js-yaml": "4.1.0",
30+
"lodash": "4.17.21",
31+
"path-to-regexp": "3.2.0",
32+
"swagger-ui-dist": "5.11.0"
3133
},
3234
"devDependencies": {
3335
"@commitlint/cli": "18.6.0",

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

+81-42
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,92 @@ import {
55
appControllerTextTranspiled
66
} from './fixtures/app.controller';
77
import {
8-
enhancedCommentsControllerText,
9-
enhancedCommentsControllerTextTranspiled
10-
} from './fixtures/enhanced-comments.controller';
11-
12-
const compilerOptions: ts.CompilerOptions = {
13-
module: ts.ModuleKind.CommonJS,
14-
target: ts.ScriptTarget.ESNext,
15-
newLine: ts.NewLineKind.LineFeed,
16-
noEmitHelpers: true
17-
}
18-
19-
const transpileModule = (filename, controllerText, compilerOptions, swaggerDocumentOptions = {}) => {
20-
const fakeProgram = ts.createProgram([filename], compilerOptions);
21-
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-
}
8+
appControllerWithTabsText,
9+
appControllerWithTabsTextTranspiled
10+
} from './fixtures/app.controller-tabs';
11+
import {
12+
appControllerWithoutModifiersText,
13+
appControllerWithoutModifiersTextTranspiled
14+
} from './fixtures/app.controller-without-modifiers';
3515

3616
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-
);
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);
4427

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+
});
4540
expect(result.outputText).toEqual(appControllerTextTranspiled);
4641
});
4742

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 }
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);
53+
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+
});
68+
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);
79+
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
5494
);
55-
expect(result.outputText).toEqual(enhancedCommentsControllerTextTranspiled);
56-
})
95+
});
5796
});

‎test/plugin/fixtures/enhanced-comments.controller.ts

-109
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.