riof
Version:
Rio framework
506 lines (438 loc) • 21 kB
text/typescript
import {CallExpression, Project, Scope, SyntaxKind, ts} from "ts-morph";
import fs from "fs-extra";
import yaml from "yaml";
import * as path from "path";
import {execSync} from "child_process";
interface RioTemplateMethod {
method: string
type?: string
description?: string
queryStringModel?: string
inputModel?: string
outputModel?: string
errorModel?: string
handler: string
}
interface RioMethodHandler {
handler: string
queryStringModel?: string
inputModel?: string
}
interface RioTemplate {
init: RioMethodHandler
get: RioMethodHandler
getInstanceId: string
getState: string
authorizer: string
destroy: string
architecture?: string
accelerated?: string
description?: string
dependencies: string[]
methods: RioTemplateMethod[]
}
export async function compile(sourcePath?: string) {
if(sourcePath) process.chdir(sourcePath);
if (fs.existsSync(path.join(process.cwd(), 'dist'))) {
fs.rmSync(path.join(process.cwd(), 'dist'), {recursive: true, force: true})
}
fs.copySync(path.join(process.cwd(), 'src'), path.join(process.cwd(), 'dist', 'dependencies', 'system', 'src'), {overwrite: true ,filter: (src, dest) => !src.includes('__tests__')})
fs.copySync(path.join(process.cwd(), 'package.json'), path.join(process.cwd(), 'dist', 'package.json'), {overwrite: true})
fs.copySync(path.join(process.cwd(), 'node_modules'), path.join(process.cwd(), 'dist', 'node_modules'), {overwrite: true})
fs.mkdirpSync(path.join(process.cwd(), 'dist', 'models'))
// `tsconfig.json` dosyasını yükle
const configFile = ts.readConfigFile('./tsconfig.json', ts.sys.readFile);
configFile.config.include = ['./**/*.ts','./**/*.yml', './**/*.json', './**/*.js']
configFile.config.compilerOptions.suppressImplicitAnyIndexErrors= true
configFile.config.compilerOptions.baseUrl = '.'
configFile.config.compilerOptions.rootDir = './'
configFile.config.compilerOptions.outDir = './dist'
configFile.config.compilerOptions.paths = {
'system': ['dependencies/system'],
'system/*': ['dependencies/system/*'],
}
fs.writeFileSync(path.join(process.cwd(), 'dist', 'tsconfig.json'), JSON.stringify(configFile.config, undefined, 2))
process.chdir('dist')
const project = new Project({tsConfigFilePath: path.join(process.cwd(), 'tsconfig.json')});
const classFiles = project.getSourceFiles();
const rioRdkMethodName = 'rioRdkMethodCallAutoGenerated'
const getIndexTs = (className: string) => {
return `
import {Data} from "@retter/rdk";
import {RioMethodError} from "riof";
import {#class_name#} from "system/src/#class_name#";
export async function exportRioMethodModels(): Promise<void> {
const ref = new #class_name#();
// @ts-ignore
await ref.exportRioMethodModels();
}
export async function init(data: Data): Promise<Data> {
try {
const ref = new #class_name#();
// @ts-ignore
await ref.init(data);
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data
}
export async function _get(data: Data): Promise<Data> {
try {
const ref = new #class_name#();
// @ts-ignore
await ref._get(data);
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data
}
export async function authorizer(data: Data): Promise<any> {
try {
const ref = new #class_name#();
// @ts-ignore
if(await ref.authorizer(data)){
return {statusCode: 200};
}else{
return {statusCode: 403}
}
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data.response
}
export async function getInstanceId(data: Data): Promise<any> {
const ref = new #class_name#();
// @ts-ignore
return await ref.getInstanceId(data);
}
export async function getState(data: Data): Promise<any> {
try {
const ref = new #class_name#();
data.response = {
statusCode: 200,
// @ts-ignore
body: await ref.getState(data)
}
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data.response
}
export async function destroy(data: Data): Promise<any> {
try {
const ref = new #class_name#();
// @ts-ignore
await ref.destroy(data);
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data.response
}
`.replace(/#class_name#/g, className)
}
const getIndexTsMethod = (className: string, methodName: string) => {
return `
export async function #method_name#(data: Data): Promise<Data> {
try {
const ref = new #class_name#();
// @ts-ignore
ref.setDataProperties(data);
const res = await ref.#method_name#(data.request);
data.response = {
statusCode: 200,
body: res
}
// @ts-ignore
data.state = ref.state
// @ts-ignore
data.schedule = ref.schedule
// @ts-ignore
data.tasks = ref.tasks
// @ts-ignore
data.events = ref.events
} catch (e: any) {
if (e instanceof RioMethodError) {
data.response = e.getRioResponse();
} else {
const error = new RioMethodError({statusCode: 500, message: 'Unhandled error'});
data.response = error.getRioResponse();
}
}
return data
}
`.replace(/#class_name#/g, className).replace(/#method_name#/g, methodName)
}
const rioTemplates: { className: string, template: RioTemplate }[] = []
const globalMethodDeclarations: any[] = []
for (let i = 0; i < classFiles.length; i++) {
for (const sourceClass of classFiles[i].getClasses()) {
const className = sourceClass.getName();
if (!className) continue;
const classDeclaration = classFiles[i].getClass(className);
if (!classDeclaration) continue;
if (!classDeclaration.getDecorators().some(d => d.getName() === 'RioClass')) continue;
classDeclaration.findReferencesAsNodes()
const classRefs = classDeclaration.findReferencesAsNodes().filter(node => node.getAncestors()[0].getKind() === SyntaxKind.NewExpression)
for (const ref of classRefs) {
const newExpression = ref.getAncestors()[0]
ref.getAncestors()[0].replaceWithText(`await ${newExpression.getText()}.getRioClassInstance()`)
}
const classTemplate: RioTemplate = {
init: {
handler: "index.init"
},
get: {
handler: "index._get"
},
authorizer: "index.authorizer",
getState: "index.getState",
getInstanceId: "index.getInstanceId",
destroy: "index.destroy",
description: "Made in Rio framework",
dependencies: [
"system"
],
methods: []
}
const classDecoratorRioArgs = classDeclaration.getDecorators().find(d => d.getName() === 'RioClass')?.getArguments()
if (classDecoratorRioArgs && classDecoratorRioArgs.length > 0) {
for (const classDecoratorRioArg of classDecoratorRioArgs) {
classDecoratorRioArg.getDescendantsOfKind(SyntaxKind.PropertyAssignment).forEach(p => {
const prop = p.getDescendants()[0].getText()
let value: any = p.getDescendants()[2].getText()
switch (p.getDescendants()[2].getKind()) {
case SyntaxKind.FalseKeyword:
value = false
break
case SyntaxKind.TrueKeyword:
value = true
break
case SyntaxKind.NumberKeyword:
value = Number(value)
break
default:
value = value.replace(/['"]+/g, '')
break
}
classTemplate[<keyof RioTemplate>prop] = value
})
}
}
let noRDKImport = true
for (const importDeclaration of classFiles[i].getImportDeclarations()) {
if (importDeclaration.getModuleSpecifierValue() === '@retter/rdk') {
const importClause = importDeclaration.getImportClause()?.getText()
if (!importClause || (importClause && importClause !== 'RDK')) {
const clause = importDeclaration.getImportClause()
if (clause) {
clause.replaceWithText(`RDK, { ${importDeclaration.getNamedImports().map(i => i.getText()).join(', ')} }`)
}
}
noRDKImport = false
break;
}
}
if (noRDKImport) {
classFiles[i].addImportDeclaration({
moduleSpecifier: '@retter/rdk',
defaultImport: "RDK"
})
}
let noFSImport = true
for (const importDeclaration of classFiles[i].getImportDeclarations()) {
if (importDeclaration.getModuleSpecifierValue() === 'fs/promises') {
const importClause = importDeclaration.getImportClause()?.getText()
if (!importClause || (importClause && importClause !== 'fs')) {
const clause = importDeclaration.getImportClause()
if (clause) {
clause.replaceWithText(`fs, { ${importDeclaration.getNamedImports().map(i => i.getText()).join(', ')} }`)
}
}
noFSImport = false
break;
}
}
if (noFSImport) {
classFiles[i].addImportDeclaration({
moduleSpecifier: 'fs/promises',
defaultImport: "fs"
})
}
let noZod2SchemaImport = true
for (const importDeclaration of classFiles[i].getImportDeclarations()) {
if (importDeclaration.getModuleSpecifierValue() === 'zod-to-json-schema') {
const importClause = importDeclaration.getImportClause()?.getText()
if (!importClause || (importClause && importClause !== 'zod-to-json-schema')) {
const clause = importDeclaration.getImportClause()
if (clause) {
clause.replaceWithText(`{ zodToJsonSchema, ${importDeclaration.getNamedImports().map(i => i.getText()).join(', ')} }`)
}
}
noZod2SchemaImport = false
break;
}
}
if (noZod2SchemaImport) {
classFiles[i].addImportDeclaration({
moduleSpecifier: 'zod-to-json-schema',
namedImports: ['zodToJsonSchema']
})
}
if (!classDeclaration.getMethod(rioRdkMethodName)) {
classDeclaration.addMethod({
docs: ["!Auto generated method!"],
name: rioRdkMethodName,
isAsync: true,
parameters: [{name: 'props', type: '{method: string, input: any}'}],
returnType: "Promise<any>",
statements: `const rdk = new RDK();
const response = await rdk.methodCall({
...props.input,
classId: '${classDeclaration.getName()}',
methodName: props.method,
instanceId: this.instanceId,
lookupKey: this.lookup
});
return response?.body;`.replace(/(\r\n|\n|\r)/gm, "").replace(/ {4}/g, '')
})
}
const uniqueModelNames: string[] = []
for (const method of classDeclaration.getMethods().filter(m => m.getDecorators().some(d => d.getName() === 'RioMethod'))) {
const methodDeclaration = classDeclaration.getMethod(method.getName());
if (!methodDeclaration) continue;
const rioDecorator = methodDeclaration.getDecorators().find(d => d.getName() === 'RioMethod')
if(!rioDecorator) continue;
const methodDecoratorRioArgs = rioDecorator.getArguments()
if (methodDecoratorRioArgs && methodDecoratorRioArgs.length > 0) {
for (const methodDecoratorRioArg of methodDecoratorRioArgs) {
methodDecoratorRioArg.getDescendantsOfKind(SyntaxKind.PropertyAssignment).forEach(p => {
const prop = p.getDescendants()[0].getText()
let value = p.getDescendants()[2].getText();
const methodIndex = classTemplate.methods.findIndex(m => m.method === method.getName())
if (methodIndex === -1) {
const methodObj: RioTemplateMethod = {
method: method.getName(),
handler: `index.${method.getName()}`
}
methodObj[<keyof RioTemplateMethod>prop] = value.replace(/['"]+/g, '')
classTemplate.methods.push(methodObj)
} else {
value = value.replace(/['"]+/g, '')
classTemplate.methods[methodIndex][<keyof RioTemplateMethod>prop] = value
}
if (['queryStringModel', 'inputModel', 'outputModel', 'errorModel'].includes(prop)) {
if (['init', 'getState', 'getInstanceId', 'destroy'].includes(method.getName())) {
classTemplate[method.getName()][<keyof RioTemplateMethod>prop] = `${className}_${value}`
} else {
classTemplate.methods[methodIndex][<keyof RioTemplateMethod>prop] = `${className}_${value}`
}
uniqueModelNames.push(value)
}
})
}
}
const methodReferences = methodDeclaration.findReferencesAsNodes()
for (const reference of methodReferences) {
const ref = reference.getAncestors()[1]
if (ref.getKind() !== SyntaxKind.CallExpression) continue;
const expression = ref as CallExpression
const argList = expression.getArguments();
const classReference = reference.getAncestors()[0].getChildren()[0].getText()
const methodName = method.getName()
expression.replaceWithText(`${classReference}.${rioRdkMethodName}({method: '${methodName}', input: ${argList.map(a => a.getText()).join(', ')}})`)
}
//change other classes
for (const classFile of classFiles) {
const classes = classFile.getClasses()
for (const cls of classes) {
if(cls.getName() === classDeclaration.getName()) continue;
cls.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((call) => {
const refValues = call.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).flatMap(exp=>exp.getDescendantsOfKind(SyntaxKind.Identifier).map(i=>i.getText()))
if(refValues.length === 2 && refValues[1] === methodDeclaration.getName()){
const argList = call.getArguments();
const classReference = refValues[0]
const methodName = method.getName()
call.replaceWithText(`${classReference}.${rioRdkMethodName}({method: '${methodName}', input: ${argList.map(a => a.getText()).join(', ')}})`)
}
})
}
}
rioDecorator.remove()
}
let exportModelMethodBody = `
await Promise.all([`
for (const uniqueModelName of uniqueModelNames) {
exportModelMethodBody += `fs.writeFile('${path.join('.', 'models', `${className}_${uniqueModelName}.json`)}', JSON.stringify(zodToJsonSchema(${uniqueModelName}, {name: '${uniqueModelName}', $refStrategy: 'none'}).definitions!['${uniqueModelName}'], undefined, 2)),`
}
exportModelMethodBody += `])`
classDeclaration.addMethod({
scope: Scope.Private,
docs: ["!Auto generated method!"],
name: 'exportRioMethodModels',
isAsync: true,
returnType: "Promise<void>",
statements: exportModelMethodBody
})
rioTemplates.push({className, template: classTemplate})
}
}
for (const rioTemplate of rioTemplates) {
const classPath = path.join(process.cwd(), 'classes', rioTemplate.className)
project.createSourceFile(path.join(classPath, 'template.yml'), yaml.stringify(rioTemplate.template), {overwrite: true})
let indexFile = getIndexTs(rioTemplate.className)
for (const method of rioTemplate.template.methods) {
if(['init', 'getState', 'getInstanceId', 'destroy'].includes(method.method)) continue;
indexFile += getIndexTsMethod(rioTemplate.className, method.method) + '\n'
}
project.createSourceFile(path.join(classPath, 'index.ts'), indexFile, {overwrite: true})
}
project.saveSync()
project.emitSync()
fs.copySync(path.join(process.cwd(), 'dist', 'dependencies'), path.join(process.cwd(), 'node_modules'), {overwrite: true})
const packageFilesWorkers = []
const writeTemplatesToDist = []
let exportModelsCode = `(async () => { await Promise.all([`
for (const rioTemplate of rioTemplates) {
exportModelsCode += `require('./classes/${rioTemplate.className}').exportRioMethodModels(),`
writeTemplatesToDist.push(
fs.writeFile(path.join(process.cwd(), 'dist', 'classes', rioTemplate.className, 'template.yml'), yaml.stringify(rioTemplate.template))
)
const globalPackageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'))
packageFilesWorkers.push(
fs.writeFile(path.join(process.cwd(), 'dist', 'classes', rioTemplate.className, 'package.json'), JSON.stringify({
"name": rioTemplate.className.toLowerCase(),
"version": "1.0.0",
"dependencies":globalPackageJson.dependencies,
},undefined, 2))
)
}
exportModelsCode += `])})().catch();`
execSync(`ts-node -e "${exportModelsCode}"`, {stdio: 'inherit'})
await Promise.all(writeTemplatesToDist)
fs.copySync(path.join(process.cwd(), 'models'), path.join(process.cwd(), 'dist', 'models'), {overwrite: true})
}