UNPKG

riof

Version:

Rio framework

506 lines (438 loc) 21 kB
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}) }