@gati-framework/cli
Version:
CLI tool for Gati framework - create, develop, build and deploy cloud-native applications
243 lines • 7.86 kB
JavaScript
/**
* @module cli/analyzer/handler-analyzer
* @description Analyze handlers and modules using ts-morph
*/
import { Project, SyntaxKind } from 'ts-morph';
import { resolve, relative } from 'path';
import { existsSync, readdirSync, statSync } from 'fs';
/**
* Analyze entire project and create manifest
*/
export function analyzeProject(projectRoot) {
const project = new Project({
tsConfigFilePath: resolve(projectRoot, 'tsconfig.json'),
skipAddingFilesFromTsConfig: true
});
const srcDir = resolve(projectRoot, 'src');
const handlers = [];
const modules = [];
// Scan all TypeScript files
const files = scanDirectory(srcDir, ['.ts', '.js']);
for (const filePath of files) {
const sourceFile = project.addSourceFileAtPath(filePath);
if (filePath.includes('/handlers/') || filePath.includes('\\handlers\\')) {
const handler = analyzeHandler(sourceFile, srcDir);
if (handler)
handlers.push(handler);
}
else if (filePath.includes('/modules/') || filePath.includes('\\modules\\')) {
const module = analyzeModule(sourceFile);
if (module)
modules.push(module);
}
}
const routeTree = buildRouteTree(handlers);
const conflicts = detectConflicts(routeTree);
return { handlers, modules, routeTree, conflicts };
}
/**
* Analyze handler file
*/
function analyzeHandler(sourceFile, srcRoot) {
const filePath = sourceFile.getFilePath();
const relativePath = relative(srcRoot, filePath);
// Find handler exports
const exports = sourceFile.getExportedDeclarations();
for (const [name] of exports) {
if (name.toLowerCase().includes('handler')) {
const method = extractMethodFromExport(sourceFile) || 'GET';
const customRoute = extractRouteFromExport(sourceFile);
return {
filePath,
relativePath,
route: customRoute ? buildFullRoute(relativePath, customRoute) : pathToRoute(relativePath),
method,
customRoute,
exportName: name,
exportType: sourceFile.getDefaultExportSymbol()?.getName() === name ? 'default' : 'named',
imports: extractImports(sourceFile),
dependencies: extractDependencies(sourceFile)
};
}
}
return null;
}
/**
* Analyze module file
*/
function analyzeModule(sourceFile) {
const filePath = sourceFile.getFilePath();
const exports = sourceFile.getExportedDeclarations();
for (const [name] of exports) {
if (!name.toLowerCase().includes('handler')) {
const methods = extractModuleMethods(sourceFile);
return {
filePath,
exportName: name,
exportType: sourceFile.getDefaultExportSymbol()?.getName() === name ? 'default' : 'named',
methods,
dependencies: extractDependencies(sourceFile)
};
}
}
return null;
}
/**
* Convert file path to route
*/
function pathToRoute(relativePath) {
let route = relativePath
.replace(/\\/g, '/')
.replace(/^handlers\//, '')
.replace(/\.ts$/, '')
.replace(/\.js$/, '')
.replace(/\/index$/, '');
// Handle dynamic routes [param] -> :param
route = route.replace(/\[([^\]]+)\]/g, ':$1');
// Ensure starts with /
if (!route.startsWith('/')) {
route = '/' + route;
}
return route === '/' ? '/' : route;
}
/**
* Extract HTTP method from METHOD export
*/
function extractMethodFromExport(sourceFile) {
const exports = sourceFile.getExportedDeclarations();
const methodExport = exports.get('METHOD');
if (methodExport && methodExport[0]) {
const declaration = methodExport[0];
if (declaration.getKind() === 273) { // VariableDeclaration
const initializer = declaration.getInitializer();
if (initializer && initializer.getLiteralValue) {
return initializer.getLiteralValue();
}
}
}
return undefined;
}
/**
* Extract route from ROUTE export
*/
function extractRouteFromExport(sourceFile) {
const exports = sourceFile.getExportedDeclarations();
const routeExport = exports.get('ROUTE');
if (routeExport && routeExport[0]) {
const declaration = routeExport[0];
if (declaration.getKind() === 273) { // VariableDeclaration
const initializer = declaration.getInitializer();
if (initializer && initializer.getLiteralValue) {
return initializer.getLiteralValue();
}
}
}
return undefined;
}
/**
* Build full route from file path and custom route
*/
function buildFullRoute(relativePath, customRoute) {
const parentPath = relativePath
.replace(/\\/g, '/')
.replace(/^handlers\//, '')
.replace(/\/[^/]*$/, '') // Remove filename
.replace(/\/index$/, ''); // Remove index
if (!parentPath) {
return customRoute;
}
return `/${parentPath}${customRoute}`;
}
/**
* Extract imports
*/
function extractImports(sourceFile) {
return sourceFile.getImportDeclarations()
.map(imp => imp.getModuleSpecifierValue())
.filter(module => module.startsWith('.'));
}
/**
* Extract external dependencies
*/
function extractDependencies(sourceFile) {
return sourceFile.getImportDeclarations()
.map(imp => imp.getModuleSpecifierValue())
.filter(module => !module.startsWith('.') && !module.startsWith('@gati-framework'));
}
/**
* Extract module methods
*/
function extractModuleMethods(sourceFile) {
const methods = [];
sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => {
methods.push(method.getName());
});
sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment).forEach(prop => {
if (prop.getParent()?.getKind() === SyntaxKind.ObjectLiteralExpression) {
methods.push(prop.getName());
}
});
return methods;
}
/**
* Build route tree from handlers
*/
function buildRouteTree(handlers) {
const root = {
path: '/',
children: new Map(),
conflicts: []
};
for (const handler of handlers) {
const segments = handler.route.split('/').filter(Boolean);
let current = root;
for (const segment of segments) {
if (!current.children.has(segment)) {
current.children.set(segment, {
path: segment,
children: new Map(),
conflicts: []
});
}
current = current.children.get(segment);
}
if (current.handler) {
current.conflicts.push(`Duplicate route: ${handler.route}`);
}
current.handler = handler;
}
return root;
}
/**
* Detect route conflicts
*/
function detectConflicts(node, conflicts = []) {
if (node.conflicts.length > 0) {
conflicts.push(...node.conflicts);
}
for (const child of node.children.values()) {
detectConflicts(child, conflicts);
}
return conflicts;
}
/**
* Scan directory for files
*/
function scanDirectory(dir, extensions) {
const files = [];
if (!existsSync(dir))
return files;
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = resolve(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...scanDirectory(fullPath, extensions));
}
else if (extensions.some(ext => entry.endsWith(ext))) {
files.push(fullPath);
}
}
return files;
}
//# sourceMappingURL=handler-analyzer.js.map