import fs from 'fs';
import path from 'path';
import { Project, TypeFormatFlags } from 'ts-morph';
let cachedProject: Project | null = null;
export function getTsMorphProject(rootPath: string): Project {
if (cachedProject) return cachedProject;
// Ensure .cache/ts-morph exists
const cacheDir = path.join(rootPath, '.cache', 'ts-morph');
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
// Create ts-morph Project with cache directory
cachedProject = new Project({
tsConfigFilePath: path.join(rootPath, 'tsconfig.json'),
skipAddingFilesFromTsConfig: true,
manipulationSettings: {
// Optional: speed up large projects
usePrefixAndSuffixTextForRename: true,
},
});
return cachedProject;
}
export interface ApiRoute {
name: any;
path: string;
methods: string[];
responseType: string;
kind: 'next-route' | 'api-client' | 'graphql-query' | 'graphql-mutation';
gqlString?: string;
environment?: 'server' | 'client' | 'universal';
}
// ---------------------------------
// Detect environment from function body
// ---------------------------------
export function detectEnvironment(fnBody: string): 'server' | 'client' | 'universal' {
if (/process\.env|prisma|fs|secret/i.test(fnBody)) return 'server'; // server-only
if (/fetch|axios|gql/i.test(fnBody)) return 'client'; // client-only
return 'universal'; // could run both
}
// ---------------------------------
// Crawl API routes
// ---------------------------------
export function crawlApi(rootPath: string): ApiRoute[] {
const results: ApiRoute[] = [];
const clientApiDir = path.join(rootPath, 'api');
if (!fs.existsSync(clientApiDir)) return results;
const files = fs.readdirSync(clientApiDir).filter((f: string) => f.endsWith('.ts'));
const project = getTsMorphProject(rootPath);
files.forEach((file: string) => {
const filePath = path.join(clientApiDir, file);
const sourceFile = project.addSourceFileAtPath(filePath);
sourceFile.getFunctions().forEach((fn) => {
if (!fn.isExported()) return;
const fnName = fn.getName() || 'anonymous';
const sig = fn.getSignature();
// --- Fix for deprecated getText() ---
const responseType = sig.getReturnType().getText(undefined, TypeFormatFlags.NoTruncation);
const bodyText = fn.getBodyText() || '';
// Detect environment per function
const environment = detectEnvironment(bodyText);
// Detect GraphQL
if (bodyText.includes('gql')) {
const isMutation = bodyText.toLowerCase().includes('mutation');
// Extract gql string using regex
const gqlMatch = bodyText.match(/gql`([\s\S]*?)`/);
const gqlString = gqlMatch ? gqlMatch[1].trim() : undefined;
results.push({
path: 'api/' + file,
methods: [fnName],
responseType,
kind: isMutation ? 'graphql-mutation' : 'graphql-query',
gqlString,
environment,
name: undefined,
});
} else {
// Regular API client
results.push({
path: 'api/' + file,
methods: [fnName],
responseType,
kind: 'api-client',
environment,
name: undefined,
});
}
});
});
return results;
}