@gati-framework/cli
Version:
CLI tool for Gati framework - create, develop, build and deploy cloud-native applications
240 lines • 8.6 kB
JavaScript
/**
* @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