1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.ModelClassVisitor = void 0;
|
4 | const path_1 = require("path");
|
5 | const ts = require("typescript");
|
6 | const decorators_1 = require("../../decorators");
|
7 | const plugin_constants_1 = require("../plugin-constants");
|
8 | const plugin_debug_logger_1 = require("../plugin-debug-logger");
|
9 | const ast_utils_1 = require("../utils/ast-utils");
|
10 | const plugin_utils_1 = require("../utils/plugin-utils");
|
11 | const type_reference_to_identifier_util_1 = require("../utils/type-reference-to-identifier.util");
|
12 | const CLASS_DECORATORS = [
|
13 | decorators_1.ObjectType.name,
|
14 | decorators_1.InterfaceType.name,
|
15 | decorators_1.InputType.name,
|
16 | decorators_1.ArgsType.name,
|
17 | ];
|
18 | class ModelClassVisitor {
|
19 | constructor() {
|
20 | this._typeImports = {};
|
21 | this._collectedMetadata = {};
|
22 | }
|
23 | get typeImports() {
|
24 | return this._typeImports;
|
25 | }
|
26 | get collectedMetadata() {
|
27 | const metadataWithImports = [];
|
28 | Object.keys(this._collectedMetadata).forEach((filePath) => {
|
29 | const metadata = this._collectedMetadata[filePath];
|
30 | const path = filePath.replace(/\.[jt]s$/, '');
|
31 | const importExpr = ts.factory.createCallExpression(ts.factory.createToken(ts.SyntaxKind.ImportKeyword), undefined, [ts.factory.createStringLiteral(path)]);
|
32 | metadataWithImports.push([importExpr, metadata]);
|
33 | });
|
34 | return metadataWithImports;
|
35 | }
|
36 | visit(sourceFile, ctx, program, pluginOptions) {
|
37 | this.importsToAdd = new Set();
|
38 | const typeChecker = program.getTypeChecker();
|
39 | const factory = ctx.factory;
|
40 | const visitNode = (node) => {
|
41 | const decorators = (0, ast_utils_1.getDecorators)(node);
|
42 | if (ts.isClassDeclaration(node) &&
|
43 | (0, ast_utils_1.hasDecorators)(decorators, CLASS_DECORATORS)) {
|
44 | const isExported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
45 | if (pluginOptions.readonly && !isExported) {
|
46 | if (pluginOptions.debug) {
|
47 | plugin_debug_logger_1.pluginDebugLogger.debug(`Skipping class "${node.name.getText()}" because it's not exported.`);
|
48 | }
|
49 | return;
|
50 | }
|
51 | const [members, amendedMetadata] = this.amendFieldsDecorators(factory, node.members, pluginOptions, sourceFile.fileName, typeChecker);
|
52 | const metadata = this.collectMetadataFromClassMembers(factory, members, pluginOptions, sourceFile.fileName, typeChecker);
|
53 | if (!pluginOptions.readonly) {
|
54 | return this.updateClassDeclaration(factory, node, members, metadata, pluginOptions);
|
55 | }
|
56 | else {
|
57 | const filePath = this.normalizeImportPath(pluginOptions.pathToSource, sourceFile.fileName);
|
58 | if (!this._collectedMetadata[filePath]) {
|
59 | this._collectedMetadata[filePath] = {};
|
60 | }
|
61 | const attributeKey = node.name.getText();
|
62 | this._collectedMetadata[filePath][attributeKey] = (0, ast_utils_1.safelyMergeObjects)(factory, metadata, amendedMetadata);
|
63 | return;
|
64 | }
|
65 | }
|
66 | else if (ts.isSourceFile(node) && !pluginOptions.readonly) {
|
67 | const visitedNode = ts.visitEachChild(node, visitNode, ctx);
|
68 | const importStatements = this.createEagerImports(factory);
|
69 | const existingStatements = Array.from(visitedNode.statements);
|
70 | return factory.updateSourceFile(visitedNode, [
|
71 | ...importStatements,
|
72 | ...existingStatements,
|
73 | ]);
|
74 | }
|
75 | if (pluginOptions.readonly) {
|
76 | ts.forEachChild(node, visitNode);
|
77 | }
|
78 | else {
|
79 | return ts.visitEachChild(node, visitNode, ctx);
|
80 | }
|
81 | };
|
82 | return ts.visitNode(sourceFile, visitNode);
|
83 | }
|
84 | addDescriptionToClassDecorators(f, node) {
|
85 | const description = (0, ast_utils_1.getJSDocDescription)(node);
|
86 | const decorators = (0, ast_utils_1.getDecorators)(node);
|
87 | if (!description) {
|
88 | return decorators;
|
89 | }
|
90 |
|
91 | return decorators.map((decorator) => {
|
92 | if (!CLASS_DECORATORS.includes((0, ast_utils_1.getDecoratorName)(decorator))) {
|
93 | return decorator;
|
94 | }
|
95 | const decoratorExpression = decorator.expression;
|
96 | const objectLiteralExpression = (0, ast_utils_1.serializePrimitiveObjectToAst)(f, {
|
97 | description,
|
98 | });
|
99 | let newArgumentsArray = [];
|
100 | if (decoratorExpression.arguments.length === 0) {
|
101 | newArgumentsArray = [objectLiteralExpression];
|
102 | }
|
103 | else {
|
104 |
|
105 |
|
106 |
|
107 | newArgumentsArray = decoratorExpression.arguments.map((argument, index) => {
|
108 | if (index + 1 != decoratorExpression.arguments.length) {
|
109 | return argument;
|
110 | }
|
111 |
|
112 | return (0, ast_utils_1.safelyMergeObjects)(f, objectLiteralExpression, argument);
|
113 | });
|
114 | }
|
115 | return f.updateDecorator(decorator, f.updateCallExpression(decoratorExpression, decoratorExpression.expression, decoratorExpression.typeArguments, newArgumentsArray));
|
116 | });
|
117 | }
|
118 | amendFieldsDecorators(f, members, pluginOptions, hostFilename,
|
119 | typeChecker) {
|
120 | const propertyAssignments = [];
|
121 | const updatedClassElements = members.map((member) => {
|
122 | const decorators = (0, ast_utils_1.getDecorators)(member);
|
123 | if ((ts.isPropertyDeclaration(member) || ts.isGetAccessor(member)) &&
|
124 | (0, ast_utils_1.hasDecorators)(decorators, [decorators_1.Field.name])) {
|
125 | try {
|
126 | return (0, ast_utils_1.updateDecoratorArguments)(f, member, decorators_1.Field.name, (decoratorArguments) => {
|
127 | const options = this.getOptionsFromFieldDecoratorOrUndefined(decoratorArguments);
|
128 | const { type, ...metadata } = this.createFieldMetadata(f, member, typeChecker, hostFilename, pluginOptions, this.getTypeFromFieldDecoratorOrUndefined(decoratorArguments));
|
129 | const serializedMetadata = (0, ast_utils_1.serializePrimitiveObjectToAst)(f, metadata);
|
130 | propertyAssignments.push(f.createPropertyAssignment(f.createIdentifier(member.name.getText()), serializedMetadata));
|
131 | return [
|
132 | type,
|
133 | options
|
134 | ? (0, ast_utils_1.safelyMergeObjects)(f, serializedMetadata, options)
|
135 | : serializedMetadata,
|
136 | ];
|
137 | });
|
138 | }
|
139 | catch (e) {
|
140 |
|
141 | }
|
142 | }
|
143 | return member;
|
144 | });
|
145 | return [
|
146 | updatedClassElements,
|
147 | f.createObjectLiteralExpression(propertyAssignments),
|
148 | ];
|
149 | }
|
150 | collectMetadataFromClassMembers(f, members, pluginOptions, hostFilename,
|
151 | typeChecker) {
|
152 | const properties = [];
|
153 | members.forEach((member) => {
|
154 | const decorators = (0, ast_utils_1.getDecorators)(member);
|
155 | const modifiers = (0, ast_utils_1.getModifiers)(member);
|
156 | if ((ts.isPropertyDeclaration(member) || ts.isGetAccessor(member)) &&
|
157 | !(0, ast_utils_1.hasModifiers)(modifiers, [
|
158 | ts.SyntaxKind.StaticKeyword,
|
159 | ts.SyntaxKind.PrivateKeyword,
|
160 | ]) &&
|
161 | !(0, ast_utils_1.hasDecorators)(decorators, [decorators_1.HideField.name, decorators_1.Field.name])) {
|
162 | try {
|
163 | const metadata = this.createFieldMetadata(f, member, typeChecker, hostFilename, pluginOptions);
|
164 | properties.push(f.createPropertyAssignment(f.createIdentifier(member.name.getText()), (0, ast_utils_1.serializePrimitiveObjectToAst)(f, metadata)));
|
165 | }
|
166 | catch (e) {
|
167 |
|
168 | }
|
169 | }
|
170 | });
|
171 | return f.createObjectLiteralExpression(properties);
|
172 | }
|
173 | updateClassDeclaration(f, node, members, propsMetadata, pluginOptions) {
|
174 | const method = f.createMethodDeclaration([f.createModifier(ts.SyntaxKind.StaticKeyword)], undefined, f.createIdentifier(plugin_constants_1.METADATA_FACTORY_NAME), undefined, undefined, [], undefined, f.createBlock([f.createReturnStatement(propsMetadata)], true));
|
175 | const decorators = pluginOptions.introspectComments
|
176 | ? this.addDescriptionToClassDecorators(f, node)
|
177 | : (0, ast_utils_1.getDecorators)(node);
|
178 | return f.updateClassDeclaration(node, [...decorators, ...(0, ast_utils_1.getModifiers)(node)], node.name, node.typeParameters, node.heritageClauses, [...members, method]);
|
179 | }
|
180 | getOptionsFromFieldDecoratorOrUndefined(decoratorArguments) {
|
181 | if (decoratorArguments.length > 1) {
|
182 | return decoratorArguments[1];
|
183 | }
|
184 | if (decoratorArguments.length === 1 &&
|
185 | !ts.isArrowFunction(decoratorArguments[0])) {
|
186 | return decoratorArguments[0];
|
187 | }
|
188 | }
|
189 | getTypeFromFieldDecoratorOrUndefined(decoratorArguments) {
|
190 | if (decoratorArguments.length > 0 &&
|
191 | ts.isArrowFunction(decoratorArguments[0])) {
|
192 | return decoratorArguments[0];
|
193 | }
|
194 | }
|
195 | createFieldMetadata(f, node, typeChecker, hostFilename = '', pluginOptions = {}, typeArrowFunction) {
|
196 | const type = typeChecker.getTypeAtLocation(node);
|
197 | const isNullable = !!node.questionToken || (0, ast_utils_1.isNull)(type) || (0, ast_utils_1.isUndefined)(type);
|
198 | if (!typeArrowFunction) {
|
199 | typeArrowFunction =
|
200 | typeArrowFunction ||
|
201 | f.createArrowFunction(undefined, undefined, [], undefined, undefined, this.getTypeUsingTypeChecker(f, node.type, typeChecker, hostFilename, pluginOptions));
|
202 | }
|
203 | const description = pluginOptions.introspectComments
|
204 | ? (0, ast_utils_1.getJSDocDescription)(node)
|
205 | : undefined;
|
206 | const deprecationReason = pluginOptions.introspectComments
|
207 | ? (0, ast_utils_1.getJsDocDeprecation)(node)
|
208 | : undefined;
|
209 | return {
|
210 | nullable: isNullable || undefined,
|
211 | type: typeArrowFunction,
|
212 | description,
|
213 | deprecationReason,
|
214 | };
|
215 | }
|
216 | getTypeUsingTypeChecker(f, node, typeChecker, hostFilename, options) {
|
217 | if (node && ts.isUnionTypeNode(node)) {
|
218 | const nullableType = (0, ast_utils_1.findNullableTypeFromUnion)(node, typeChecker);
|
219 | const remainingTypes = node.types.filter((item) => item !== nullableType);
|
220 | if (remainingTypes.length === 1) {
|
221 | return this.getTypeUsingTypeChecker(f, remainingTypes[0], typeChecker, hostFilename, options);
|
222 | }
|
223 | }
|
224 | const type = typeChecker.getTypeAtLocation(node);
|
225 | if (!type) {
|
226 | return undefined;
|
227 | }
|
228 | const typeReferenceDescriptor = (0, plugin_utils_1.getTypeReferenceAsString)(type, typeChecker);
|
229 | if (!typeReferenceDescriptor.typeName) {
|
230 | return undefined;
|
231 | }
|
232 | return (0, type_reference_to_identifier_util_1.typeReferenceToIdentifier)(typeReferenceDescriptor, hostFilename, options, f, type, this._typeImports, this.importsToAdd);
|
233 | }
|
234 | createEagerImports(f) {
|
235 | if (!this.importsToAdd.size) {
|
236 | return [];
|
237 | }
|
238 | return Array.from(this.importsToAdd).map((path, index) => {
|
239 | return (0, ast_utils_1.createImportEquals)(f, 'eager_import_' + index, path);
|
240 | });
|
241 | }
|
242 | normalizeImportPath(pathToSource, path) {
|
243 | let relativePath = path_1.posix.relative((0, plugin_utils_1.convertPath)(pathToSource), (0, plugin_utils_1.convertPath)(path));
|
244 | relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
|
245 | return relativePath;
|
246 | }
|
247 | }
|
248 | exports.ModelClassVisitor = ModelClassVisitor;
|