UNPKG

12.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.ModelClassVisitor = void 0;
4const path_1 = require("path");
5const ts = require("typescript");
6const decorators_1 = require("../../decorators");
7const plugin_constants_1 = require("../plugin-constants");
8const plugin_debug_logger_1 = require("../plugin-debug-logger");
9const ast_utils_1 = require("../utils/ast-utils");
10const plugin_utils_1 = require("../utils/plugin-utils");
11const type_reference_to_identifier_util_1 = require("../utils/type-reference-to-identifier.util");
12const CLASS_DECORATORS = [
13 decorators_1.ObjectType.name,
14 decorators_1.InterfaceType.name,
15 decorators_1.InputType.name,
16 decorators_1.ArgsType.name,
17];
18class 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 // get one of allowed decorators from list
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 // Options always a last parameter:
105 // @ObjectType('name', {description: ''});
106 // @ObjectType({description: ''});
107 newArgumentsArray = decoratorExpression.arguments.map((argument, index) => {
108 if (index + 1 != decoratorExpression.arguments.length) {
109 return argument;
110 }
111 // merge existing props with new props
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, // sourceFile.fileName,
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 // omit error
141 }
142 }
143 return member;
144 });
145 return [
146 updatedClassElements,
147 f.createObjectLiteralExpression(propertyAssignments),
148 ];
149 }
150 collectMetadataFromClassMembers(f, members, pluginOptions, hostFilename, // sourceFile.fileName,
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 // omit error
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}
248exports.ModelClassVisitor = ModelClassVisitor;