genezio
Version:
Command line utility to interact with Genezio infrastructure.
453 lines (452 loc) • 20.9 kB
JavaScript
import { SourceType, AstNodeType, MethodKindEnum, } from "../../models/genezioModels.js";
import typescript from "typescript";
import { readdirSync } from "fs";
import path from "path";
import { statSync } from "fs";
import { GENEZIO_CLASS_STATIC_METHOD_NOT_SUPPORTED, UserError } from "../../errors.js";
// This properties should be populated only once and then reused
let _files;
let _typeChecker;
export function clearTypescriptAstResources() {
_files = undefined;
_typeChecker = undefined;
}
export class AstGenerator {
get files() {
// Lazy initialization: execute logic only once
if (!_files) {
const typescriptFiles = [];
this.getAllFiles(typescriptFiles, ".");
const program = typescript.createProgram(typescriptFiles, {});
_typeChecker = program.getTypeChecker();
_files = program.getSourceFiles();
}
return _files;
}
get typeChecker() {
// Lazy initialization: execute logic only once
if (!_typeChecker) {
const typescriptFiles = [];
this.getAllFiles(typescriptFiles, ".");
const program = typescript.createProgram(typescriptFiles, {});
_typeChecker = program.getTypeChecker();
_files = program.getSourceFiles();
}
return _typeChecker;
}
mapTypesToParamType(type, typeChecker, declarations) {
switch (type.kind) {
case typescript.SyntaxKind.LiteralType: {
const literalType = type;
return {
type: AstNodeType.CustomNodeLiteral,
rawValue: literalType.literal.getText(),
};
}
case typescript.SyntaxKind.BigIntKeyword:
return { type: AstNodeType.BigIntLiteral };
case typescript.SyntaxKind.StringKeyword:
return { type: AstNodeType.StringLiteral };
case typescript.SyntaxKind.NumberKeyword:
return { type: AstNodeType.DoubleLiteral };
case typescript.SyntaxKind.BooleanKeyword:
return { type: AstNodeType.BooleanLiteral };
//case arrays
case typescript.SyntaxKind.ArrayType:
return {
type: AstNodeType.ArrayType,
generic: this.mapTypesToParamType(type.elementType, typeChecker, declarations),
};
case typescript.SyntaxKind.AnyKeyword:
return { type: AstNodeType.AnyLiteral };
case typescript.SyntaxKind.VoidKeyword:
return { type: AstNodeType.VoidLiteral };
case typescript.SyntaxKind.TypeReference: {
const escapedText = type.typeName.getText();
const typeArguments = type.typeArguments;
if (escapedText === "Promise") {
if (!typeArguments || typeArguments.length === 0) {
return { type: AstNodeType.AnyLiteral };
}
return {
type: AstNodeType.PromiseType,
generic: this.mapTypesToParamType(typeArguments[0], typeChecker, declarations),
};
}
else if (escapedText === "Array") {
if (!typeArguments || typeArguments.length === 0) {
return { type: AstNodeType.AnyLiteral };
}
return {
type: AstNodeType.ArrayType,
generic: this.mapTypesToParamType(typeArguments[0], typeChecker, declarations),
};
}
else if (escapedText === "Date") {
return { type: AstNodeType.DateType };
}
const typeAtLocation = typeChecker.getTypeAtLocation(type.typeName);
let typeAtLocationPath = typeAtLocation.aliasSymbol?.declarations?.[0].getSourceFile().fileName;
if (typeAtLocationPath?.endsWith(".ts")) {
typeAtLocationPath = typeAtLocationPath.slice(0, -3);
if (typeAtLocationPath.endsWith(".d")) {
typeAtLocationPath = typeAtLocationPath.slice(0, -2);
}
// We do this because typescript ignores the node_modules folder
// And the types that are present in the node_modules will dissapear
if (typeAtLocationPath.includes("node_modules")) {
typeAtLocationPath = typeAtLocationPath.replace("node_modules", "src");
}
}
if (!typeAtLocationPath) {
throw new UserError(`Type ${escapedText} is not supported by genezio. Take a look at the documentation to see the supported types. https://docs.genezio.com/`);
}
const pathFile = path
.relative(process.cwd(), typeAtLocationPath)
.replace(/\\/g, "/");
if (!this.isDeclarationInList(escapedText, pathFile, declarations)) {
let declaredNode;
if (typeAtLocation.aliasSymbol?.declarations?.[0] &&
typescript.isTypeAliasDeclaration(typeAtLocation.aliasSymbol?.declarations?.[0])) {
declaredNode = this.parseTypeAliasDeclaration(typeAtLocation.aliasSymbol?.declarations?.[0], typeChecker, declarations);
}
else if (typeAtLocation.aliasSymbol?.declarations?.[0] &&
typescript.isEnumDeclaration(typeAtLocation.aliasSymbol?.declarations?.[0])) {
declaredNode = this.parseEnumDeclaration(typeAtLocation.aliasSymbol?.declarations?.[0]);
}
else {
return {
type: AstNodeType.CustomNodeLiteral,
rawValue: type.typeName.getText(),
};
}
declaredNode.name = escapedText;
declaredNode.path = pathFile;
declarations.push(declaredNode);
}
return {
type: AstNodeType.CustomNodeLiteral,
rawValue: type.typeName.getText(),
};
}
case typescript.SyntaxKind.TypeLiteral: {
const properties = [];
for (const member of type.members) {
if (typescript.isPropertySignature(member) && member.type) {
properties.push({
name: typescript.isIdentifier(member.name)
? member.name.text
: "undefined",
optional: member.questionToken ? true : false,
type: this.mapTypesToParamType(member.type, typeChecker, declarations),
});
}
if (typescript.isIndexSignatureDeclaration(member)) {
properties.push({
name: member.name && typescript.isIdentifier(member.name)
? member.name.text
: "undefined",
optional: member.questionToken ? true : false,
type: this.mapTypesToParamType(member, typeChecker, declarations),
});
}
}
return {
type: AstNodeType.TypeLiteral,
properties: properties,
};
}
case typescript.SyntaxKind.UnionType: {
const params = [];
for (const typeNode of type.types) {
params.push(this.mapTypesToParamType(typeNode, typeChecker, declarations));
}
return { type: AstNodeType.UnionType, params: params };
}
case typescript.SyntaxKind.IndexSignature: {
return {
type: AstNodeType.MapType,
genericKey: this.mapTypesToParamType(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// In this case the type is always defined, because an IndexSignature is
// guaranteed to have only one parameter with a type of either string,
// number or symbol.
type.parameters[0].type, typeChecker, declarations),
genericValue: this.mapTypesToParamType(type.type, typeChecker, declarations),
};
}
default:
return { type: AstNodeType.AnyLiteral };
}
}
isDeclarationInList(name, path, declarations) {
for (const declarationInList of declarations) {
if (declarationInList.name === name && declarationInList.path === path) {
return true;
}
}
return false;
}
parseClassDeclaration(classDeclaration, typeChecker, declarations) {
const copy = { ...classDeclaration };
if (copy.modifiers) {
for (const modifier of copy.modifiers) {
if (modifier.kind === typescript.SyntaxKind.ExportKeyword) {
const methods = [];
for (const member of copy.members) {
if (typescript.isMethodDeclaration(member)) {
const method = this.parseMethodDeclaration(member, typeChecker, declarations);
if (method) {
methods.push(method);
}
}
}
if (classDeclaration.name === undefined) {
throw new UserError("Class name is undefined");
}
const typeAtLocation = typeChecker.getTypeAtLocation(classDeclaration.name);
let typeAtLocationPath = typeAtLocation.symbol.declarations?.[0].getSourceFile().fileName;
if (typeAtLocationPath?.endsWith(".ts")) {
typeAtLocationPath = typeAtLocationPath.slice(0, -3);
}
if (!typeAtLocationPath) {
throw new UserError("Could not find class declaration file path");
}
const pathFile = path
.relative(process.cwd(), typeAtLocationPath)
.replace(/\\/g, "/");
let docString = undefined;
const sourceFile = this.rootNode;
if (sourceFile !== undefined) {
const commentRanges = typescript.getLeadingCommentRanges(sourceFile.getFullText(), copy.pos);
// Iterate over all the comments and retain only the last JSDoc comment
for (const comment of commentRanges ?? []) {
const commentText = extractDocStringFromJsDoc(sourceFile.getFullText().slice(comment.pos, comment.end));
docString = commentText || docString;
}
}
return {
type: AstNodeType.ClassDefinition,
name: copy.name?.text ?? "undefined",
methods: methods,
path: pathFile,
docString: docString,
};
}
}
}
return undefined;
}
parseTypeAliasDeclaration(typeAliasDeclaration, typeChecker, declarations) {
const typeAliasDeclarationCopy = { ...typeAliasDeclaration };
if (typescript.isTypeLiteralNode(typeAliasDeclarationCopy.type)) {
const structLiteral = {
type: AstNodeType.StructLiteral,
name: "",
typeLiteral: {
type: AstNodeType.TypeLiteral,
properties: [],
},
};
for (const member of typeAliasDeclarationCopy.type.members) {
if ((typescript.isPropertySignature(member) ||
typescript.isIndexSignatureDeclaration(member)) &&
member.type) {
if (member.name && typescript.isComputedPropertyName(member.name)) {
throw new UserError("Computed property names are not supported");
}
const field = {
name: member.name?.text ?? "undefined",
optional: member.questionToken ? true : false,
type: member.kind === typescript.SyntaxKind.IndexSignature
? this.mapTypesToParamType(member, typeChecker, declarations)
: this.mapTypesToParamType(member.type, typeChecker, declarations),
};
structLiteral.typeLiteral.properties.push(field);
}
}
return structLiteral;
}
else {
return {
type: AstNodeType.TypeAlias,
name: "",
aliasType: this.mapTypesToParamType(typeAliasDeclarationCopy.type, typeChecker, declarations),
};
}
}
parseMethodDeclaration(methodDeclaration, typeChecker, declarations) {
const parameters = [];
const methodDeclarationCopy = { ...methodDeclaration };
if (methodDeclarationCopy.name.kind === typescript.SyntaxKind.PrivateIdentifier ||
methodDeclarationCopy.modifiers?.[0].kind === typescript.SyntaxKind.PrivateKeyword) {
return undefined;
}
if (methodDeclarationCopy.modifiers?.some((modifier) => modifier.kind === typescript.SyntaxKind.StaticKeyword)) {
throw new UserError(GENEZIO_CLASS_STATIC_METHOD_NOT_SUPPORTED);
}
for (const parameter of methodDeclarationCopy.parameters) {
if (parameter.type) {
const param = {
type: AstNodeType.ParameterDefinition,
name: typescript.isIdentifier(parameter.name)
? parameter.name.text
: "undefined",
rawType: "",
paramType: this.mapTypesToParamType(parameter.type, typeChecker, declarations),
optional: parameter.questionToken ? true : false,
defaultValue: parameter.initializer
? {
value: parameter.initializer.getText(),
type: parameter.initializer.kind === typescript.SyntaxKind.StringLiteral
? AstNodeType.StringLiteral
: AstNodeType.AnyLiteral,
}
: undefined,
};
parameters.push(param);
}
}
let methodName = "";
switch (methodDeclarationCopy.name.kind) {
case typescript.SyntaxKind.Identifier:
case typescript.SyntaxKind.StringLiteral:
case typescript.SyntaxKind.NumericLiteral:
methodName = methodDeclarationCopy.name.text;
break;
case typescript.SyntaxKind.ComputedPropertyName:
throw new UserError("Computed property names as method names are not supported");
}
let docString = undefined;
const sourceFile = this.rootNode;
if (sourceFile !== undefined) {
const commentRanges = typescript.getLeadingCommentRanges(sourceFile.getFullText(), methodDeclarationCopy.pos);
// Iterate over all the comments and retain only the last JSDoc comment
for (const comment of commentRanges ?? []) {
const commentText = extractDocStringFromJsDoc(sourceFile.getFullText().slice(comment.pos, comment.end));
docString = commentText || docString;
}
}
return {
type: AstNodeType.MethodDefinition,
docString: docString,
name: methodName,
params: parameters,
returnType: methodDeclarationCopy.type
? this.mapTypesToParamType(methodDeclarationCopy.type, typeChecker, declarations)
: { type: AstNodeType.AnyLiteral },
static: false,
kind: MethodKindEnum.method,
};
}
parseEnumDeclaration(enumDeclaration) {
const enumDeclarationCopy = { ...enumDeclaration };
return {
type: AstNodeType.Enum,
name: enumDeclarationCopy.name.text,
cases: enumDeclarationCopy.members.map((member, index) => {
if (typescript.isComputedPropertyName(member.name)) {
throw new UserError("Computed property names as enum cases are not supported");
}
if (!member.initializer) {
return {
name: member.name.text,
value: index,
type: AstNodeType.DoubleLiteral,
};
}
switch (member.initializer.kind) {
case typescript.SyntaxKind.NumericLiteral:
return {
name: member.name.text,
value: member.initializer.text,
type: AstNodeType.DoubleLiteral,
};
case typescript.SyntaxKind.StringLiteral:
return {
name: member.name.text,
value: member.initializer.text,
type: AstNodeType.StringLiteral,
};
default:
throw new UserError("Unsupported enum value type");
}
}),
};
}
getAllFiles(files, dirPath) {
const ignoreFolders = [
"node_modules",
".git",
".svn",
".vscode",
".idea",
".cache",
"tmp",
".temp",
".history",
".nyc_output",
".terraform",
".serverless",
];
// Check if the current directory is in the ignore list
if (ignoreFolders.some((folder) => dirPath.indexOf(folder) !== -1)) {
return;
}
readdirSync(dirPath).forEach((file) => {
const absolute = path.join(dirPath, file);
if (statSync(absolute).isDirectory()) {
this.getAllFiles(files, absolute);
}
else if (file.endsWith(".ts")) {
files.push(absolute);
}
});
}
async generateAst(input) {
this.files.forEach((file) => {
if (path.resolve(file.fileName) === path.resolve(input.class.path)) {
this.rootNode = file;
}
});
if (!this.rootNode) {
throw new UserError("No root node found");
}
let classDefinition = undefined;
const declarations = [];
this.rootNode.forEachChild((child) => {
if (typescript.isClassDeclaration(child)) {
const classDeclaration = this.parseClassDeclaration(child, this.typeChecker, declarations);
if (classDeclaration && !classDefinition) {
classDefinition = classDeclaration;
}
}
});
if (!classDefinition) {
throw new UserError("No exported class found in file.");
}
return {
program: {
originalLanguage: "typescript",
sourceType: SourceType.module,
body: [classDefinition, ...declarations],
},
};
}
}
function extractDocStringFromJsDoc(jsDoc) {
// Only match comments that start with `/**` and end with `*/` (i.e. JSDoc comments)
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//;
const match = jsDoc.match(jsDocRegex);
if (match) {
// Remove the leading ` * ` from each line
return match[1]
.replace(/(^|\n)\s*\*\s*/g, "$1")
.split("\n")
.map((l) => l.trim())
.join("\n");
}
return undefined;
}
const supportedExtensions = ["ts"];
export default { supportedExtensions, AstGenerator };