genezio
Version:
Command line utility to interact with Genezio infrastructure.
256 lines (255 loc) • 12.5 kB
JavaScript
import path from "path";
import fs from "fs";
import { isCallExpression, isIdentifier, isClassDeclaration, isObjectExpression, isObjectProperty, isStringLiteral, isExportDefaultDeclaration, isExportNamedDeclaration, isNumericLiteral, } from "@babel/types";
import babel from "@babel/core";
import { createRequire } from "module";
import { debugLogger } from "../logging.js";
import { DecoratorExtractor } from "./baseDecoratorExtractor.js";
export class JsTsDecoratorExtractor extends DecoratorExtractor {
fileFilter(cwd) {
return (file) => {
const folderPath = path.join(cwd, file.path);
return ((file.extension === ".js" || file.extension === ".ts") &&
!file.path.includes("node_modules") &&
!file.path.includes(".git") &&
!fs.lstatSync(folderPath).isDirectory());
};
}
async getDecoratorsFromFile(file) {
const babelClasses = await this.getDecoratorsBabel(file);
return [...babelClasses];
}
async getDecoratorsBabel(file) {
const inputCode = fs.readFileSync(file, "utf8");
const classes = [];
const extractorFunction = function extract() {
return {
name: "extract-decorators",
visitor: {
ClassDeclaration(path) {
if (path.node.decorators) {
const info = {
path: file,
name: path.node.id?.name ?? "default",
decorators: [],
methods: [],
};
for (const decorator of path.node.decorators) {
if (isIdentifier(decorator.expression)) {
info.decorators = [
...(info.decorators ?? []),
{ name: decorator.expression.name },
];
}
else if (isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.callee)) {
info.decorators = [
...(info.decorators ?? []),
{
name: decorator.expression.callee.name,
arguments: JsTsDecoratorExtractor.parseArguments(decorator.expression.arguments),
},
];
}
}
classes.push(info);
}
},
ClassMethod(path) {
if (path.node.decorators) {
const classDeclarationNode = path.context.parentPath.parentPath?.node;
const className = isClassDeclaration(classDeclarationNode)
? (classDeclarationNode.id?.name ?? "defaultClass")
: "defaultClass";
const methodIdentifierNode = path.node.key;
const methodName = isIdentifier(methodIdentifierNode)
? methodIdentifierNode.name
: "defaultMethod";
for (const decorator of path.node.decorators) {
const info = {
name: methodName,
decorators: [],
};
let existingClass = classes.find((c) => c.name === className);
if (!existingClass) {
existingClass = {
path: file,
name: className,
decorators: [],
methods: [],
};
classes.push(existingClass);
}
if (isIdentifier(decorator.expression)) {
info.decorators = [
...(info.decorators ?? []),
{ name: decorator.expression.name },
];
}
else if (isCallExpression(decorator.expression) &&
isIdentifier(decorator.expression.callee)) {
info.decorators = [
...(info.decorators ?? []),
{
name: decorator.expression.callee.name,
arguments: JsTsDecoratorExtractor.parseArguments(decorator.expression.arguments),
},
];
}
existingClass.methods.push(info);
}
}
},
},
};
};
const extractorFunctionComments = function extract() {
return {
name: "extract-comments",
visitor: {
ExportDeclaration(path) {
if (isExportDefaultDeclaration(path.node) ||
isExportNamedDeclaration(path.node)) {
const classDeclarationNode = path.node.declaration;
if (isClassDeclaration(classDeclarationNode)) {
const className = classDeclarationNode.id?.name ?? "default";
const comments = path.node.leadingComments;
if (comments && comments.length > 0) {
const lastComment = comments[comments.length - 1];
if (lastComment.value.includes("genezio: deploy") &&
lastComment.type === "CommentLine") {
const classInfo = DecoratorExtractor.createGenezioClassInfo(className, file, lastComment.value);
classes.push(classInfo);
}
}
}
}
},
ClassDeclaration(path) {
if (path.node.leadingComments) {
const leadingComments = path.node.leadingComments;
if (leadingComments.length > 0) {
const lastComment = leadingComments[leadingComments.length - 1];
if (lastComment.value.includes("genezio: deploy") &&
lastComment.type === "CommentLine") {
const className = path.node.id?.name ?? "default";
const classInfo = DecoratorExtractor.createGenezioClassInfo(className, file, lastComment.value);
classes.push(classInfo);
}
}
}
},
ClassMethod(path) {
const classDeclarationNode = path.context.parentPath.parentPath?.node;
const className = isClassDeclaration(classDeclarationNode)
? (classDeclarationNode.id?.name ?? "defaultClass")
: "defaultClass";
const methodIdentifierNode = path.node.key;
const methodName = isIdentifier(methodIdentifierNode)
? methodIdentifierNode.name
: "defaultMethod";
let existingClass = classes.find((c) => c.name === className);
if (!existingClass) {
existingClass = {
path: file,
name: className,
decorators: [],
methods: [],
};
classes.push(existingClass);
}
if (path.node.leadingComments) {
const leadingComments = path.node.leadingComments;
if (leadingComments.length > 0) {
const lastComment = leadingComments[leadingComments.length - 1];
if (lastComment.value.includes("genezio:") &&
lastComment.type === "CommentLine") {
const methodInfo = DecoratorExtractor.createGenezioMethodInfo(methodName, lastComment.value);
if (methodInfo) {
existingClass.methods.push(methodInfo);
}
}
}
}
},
},
};
};
const require = createRequire(import.meta.url);
const packagePath = path.dirname(require.resolve("@babel/plugin-syntax-decorators"));
const packageReactPath = path.dirname(require.resolve("@babel/preset-react"));
await babel
.transformAsync(inputCode, {
presets: [
[require.resolve("@babel/preset-typescript"), { allowDeclareFields: true }],
[packageReactPath],
],
plugins: [
[packagePath, { version: "2023-05", decoratorsBeforeExport: false }],
extractorFunction,
],
filename: file,
configFile: false,
})
.catch((error) => {
if (error.reasonCode == "MissingOneOfPlugins") {
debugLogger.error(`Error while parsing the file ${file}`, error);
return [];
}
else {
throw error;
}
});
await babel
.transformAsync(inputCode, {
presets: [
[require.resolve("@babel/preset-typescript"), { allowDeclareFields: true }],
[packageReactPath],
],
plugins: [
[packagePath, { version: "2023-05", decoratorsBeforeExport: false }],
extractorFunctionComments,
],
filename: file,
configFile: false,
})
.catch((error) => {
if (error.reasonCode == "MissingOneOfPlugins") {
debugLogger.error(`Error while parsing the file ${file}`, error);
return [];
}
else {
throw error;
}
});
return classes;
}
static parseArguments(args) {
if (!args) {
return {};
}
return args
.map((arg) => {
if (isObjectExpression(arg)) {
return arg.properties.reduce((acc, curr) => {
if (isObjectProperty(curr) &&
(isIdentifier(curr.key) || isStringLiteral(curr.key)) &&
(isStringLiteral(curr.value) || isNumericLiteral(curr.value))) {
const key = isIdentifier(curr.key) ? curr.key.name : curr.key.value;
return [...acc, { key, value: curr.value.value }];
}
return [...acc];
}, []);
}
else {
return undefined;
}
})
.filter((a) => a !== undefined)
.flat()
.reduce((acc, curr) => {
acc[curr.key] = curr.value;
return acc;
}, {});
}
}