UNPKG

@gati-framework/cli

Version:

CLI tool for Gati framework - create, develop, build and deploy cloud-native applications

240 lines 8.6 kB
/** * @module cli/analyzer/manifest-generator * @description Generates handler and module manifests from TypeScript code * * Implements Task 8: Handler Manifest Generation * - Extracts handler metadata (ID, path, methods) * - Generates GType references * - Extracts hook definitions * - Generates security policies * - Creates Timescape fingerprint */ import * as ts from 'typescript'; import * as path from 'path'; import * as fs from 'fs'; import * as crypto from 'crypto'; import { extractHooks } from './hook-extractor.js'; /** * Manifest generator for handlers and modules */ export class ManifestGenerator { program; checker; options; constructor(options) { this.options = options; // Load TypeScript config const tsConfigPath = options.tsConfigPath || path.join(options.projectRoot, 'tsconfig.json'); const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsConfigPath)); // Create TypeScript program this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options); this.checker = this.program.getTypeChecker(); } /** * Generate handler manifest from source file */ generateHandlerManifest(filePath) { const sourceFile = this.program.getSourceFile(filePath); if (!sourceFile) { return null; } let handlerInfo = { hooks: { before: [], after: [], catch: [] }, dependencies: { modules: [], plugins: [] }, policies: {}, }; // Visit all nodes in the source file const visit = (node) => { // Look for handler function exports if (ts.isFunctionDeclaration(node) && node.name) { const name = node.name.text; // Check if this is a handler (has req, res, lctx, gctx parameters) if (this.isHandlerFunction(node)) { handlerInfo.handlerId = this.generateHandlerId(filePath, name); // Extract path and method from JSDoc or decorators const jsdoc = this.extractJSDoc(node); handlerInfo.path = jsdoc.path || `/${name}`; handlerInfo.method = jsdoc.method || 'GET'; // Extract GType references handlerInfo.gtypes = this.extractGTypes(node); // Extract hooks handlerInfo.hooks = this.extractHooks(node); // Extract policies handlerInfo.policies = this.extractPolicies(jsdoc); // Extract dependencies handlerInfo.dependencies = this.extractDependencies(sourceFile); } } ts.forEachChild(node, visit); }; visit(sourceFile); // Generate Timescape version (fingerprint) if (handlerInfo.handlerId) { handlerInfo.timescapeVersion = this.generateTimescapeVersion(sourceFile); return handlerInfo; } return null; } /** * Check if function is a handler */ isHandlerFunction(node) { if (!node.parameters || node.parameters.length !== 4) { return false; } const paramNames = node.parameters.map(p => p.name.kind === ts.SyntaxKind.Identifier ? p.name.text : ''); return (paramNames[0] === 'req' && paramNames[1] === 'res' && paramNames[2] === 'lctx' && paramNames[3] === 'gctx'); } /** * Generate handler ID from file path and function name */ generateHandlerId(filePath, functionName) { const relativePath = path.relative(this.options.projectRoot, filePath); const pathParts = relativePath.split(path.sep).filter(p => p !== 'handlers'); const baseName = path.basename(filePath, path.extname(filePath)); return `${pathParts.join('.')}.${functionName}`; } /** * Extract JSDoc comments */ extractJSDoc(node) { const jsdoc = {}; const jsDocTags = ts.getJSDocTags(node); for (const tag of jsDocTags) { const tagName = tag.tagName.text; const comment = tag.comment; if (tagName === 'path' && typeof comment === 'string') { jsdoc.path = comment; } else if (tagName === 'method' && typeof comment === 'string') { jsdoc.method = comment.toUpperCase(); } else if (tagName === 'roles' && typeof comment === 'string') { jsdoc.roles = comment.split(',').map(r => r.trim()); } else if (tagName === 'rateLimit' && typeof comment === 'string') { const [limit, window] = comment.split('/').map(s => parseInt(s.trim())); jsdoc.rateLimit = { limit, window }; } } return jsdoc; } /** * Extract GType references from handler parameters */ extractGTypes(node) { const gtypes = { request: 'any', response: 'any', }; // Extract request type from first parameter if (node.parameters[0]?.type) { gtypes.request = this.typeToString(node.parameters[0].type); } // Extract response type from return type if (node.type) { gtypes.response = this.typeToString(node.type); } return gtypes; } /** * Convert TypeScript type node to string */ typeToString(typeNode) { return typeNode.getText(); } /** * Extract hook definitions from handler body */ extractHooks(node) { const hooks = { before: [], after: [], catch: [], }; // Get the source file containing this function const sourceFile = node.getSourceFile(); if (!sourceFile) { return hooks; } // Extract hooks using hook-extractor const extractedHooks = extractHooks(sourceFile); // Group hooks by type for (const hook of extractedHooks) { if (hook.type === 'before') { hooks.before.push(hook.id); } else if (hook.type === 'after') { hooks.after.push(hook.id); } else if (hook.type === 'catch') { if (!hooks.catch) { hooks.catch = []; } hooks.catch.push(hook.id); } } return hooks; } /** * Extract security policies from JSDoc */ extractPolicies(jsdoc) { const policies = {}; if (jsdoc.roles) { policies.roles = jsdoc.roles; } if (jsdoc.rateLimit) { policies.rateLimit = jsdoc.rateLimit; } return policies; } /** * Extract module and plugin dependencies */ extractDependencies(sourceFile) { const dependencies = { modules: [], plugins: [], }; // Look for gctx.modules.* usage const visit = (node) => { if (ts.isPropertyAccessExpression(node)) { const text = node.getText(); if (text.startsWith('gctx.modules.')) { const moduleName = text.split('.')[2]; if (moduleName && !dependencies.modules.includes(moduleName)) { dependencies.modules.push(moduleName); } } } ts.forEachChild(node, visit); }; visit(sourceFile); return dependencies; } /** * Generate Timescape version fingerprint */ generateTimescapeVersion(sourceFile) { const content = sourceFile.getFullText(); const hash = crypto.createHash('sha256').update(content).digest('hex'); return `v${Date.now()}-${hash.substring(0, 8)}`; } /** * Write manifest to file */ writeManifest(manifest, filename) { const outputDir = this.options.outputDir || path.join(this.options.projectRoot, '.gati', 'manifests'); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const outputPath = path.join(outputDir, filename); fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2)); } } //# sourceMappingURL=manifest-generator.js.map