quickwire
Version:
Automatic API generator for Next.js applications that creates API routes and TypeScript client functions from backend functions
1,170 lines (1,157 loc) β’ 63.2 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fileOperationQueue = exports.changedFiles = exports.deletedFiles = exports.backendToGenerated = exports.docCache = exports.generatedFiles = void 0;
exports.markFileChanged = markFileChanged;
exports.markFileDeleted = markFileDeleted;
exports.generateQuickwireFile = generateQuickwireFile;
exports.generateApiRoutesForFile = generateApiRoutesForFile;
exports.processBackendFile = processBackendFile;
exports.scanAllBackendFunctions = scanAllBackendFunctions;
exports.getGenerationStats = getGenerationStats;
exports.validateGeneratedFiles = validateGeneratedFiles;
exports.recoverFromCorruption = recoverFromCorruption;
exports.ensureDirectoryExists = ensureDirectoryExists;
exports.safeWriteFile = safeWriteFile;
exports.safeRemoveFile = safeRemoveFile;
exports.calculateFileChecksum = calculateFileChecksum;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const config_1 = require("./config");
const utils_1 = require("./utils/utils");
const ast_1 = require("./ast");
const cache_1 = require("./cache");
exports.generatedFiles = new Map();
exports.docCache = new Map();
exports.backendToGenerated = new Map();
exports.deletedFiles = new Set();
exports.changedFiles = new Set();
exports.fileOperationQueue = new Set();
let lastFullScan = 0;
let isProcessing = false;
// Utility functions for better file management
function calculateFileChecksum(filePath) {
try {
const content = fs_1.default.readFileSync(filePath, 'utf8');
// Simple checksum using content hash
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(16);
}
catch (error) {
console.warn(`β οΈ Failed to calculate checksum for ${filePath}:`, error);
return Date.now().toString();
}
}
function ensureDirectoryExists(dirPath) {
try {
if (!fs_1.default.existsSync(dirPath)) {
fs_1.default.mkdirSync(dirPath, { recursive: true });
console.log(`π Created directory: ${path_1.default.relative(process.cwd(), dirPath)}`);
}
return true;
}
catch (error) {
console.error(`β Failed to create directory ${dirPath}:`, error);
return false;
}
}
function safeWriteFile(filePath, content, sourceFile) {
try {
const dirPath = path_1.default.dirname(filePath);
if (!ensureDirectoryExists(dirPath)) {
return false;
}
// Create temporary file first
const tempPath = `${filePath}.tmp`;
fs_1.default.writeFileSync(tempPath, content, "utf8");
// Atomic move
fs_1.default.renameSync(tempPath, filePath);
// Track the generated file
const metadata = {
generatedAt: Date.now(),
sourceFile,
checksum: calculateFileChecksum(filePath)
};
exports.generatedFiles.set(filePath, metadata);
console.log(`β
Generated: ${path_1.default.relative(process.cwd(), filePath)}`);
return true;
}
catch (error) {
console.error(`β Failed to write file ${filePath}:`, error);
// Clean up temp file if it exists
try {
const tempPath = `${filePath}.tmp`;
if (fs_1.default.existsSync(tempPath)) {
fs_1.default.unlinkSync(tempPath);
}
}
catch (cleanupError) {
console.warn(`β οΈ Failed to cleanup temp file:`, cleanupError);
}
return false;
}
}
function safeRemoveFile(filePath) {
try {
if (fs_1.default.existsSync(filePath)) {
// Check if it's actually a generated file
const stat = fs_1.default.statSync(filePath);
if (!stat.isFile()) {
console.warn(`β οΈ Skipping removal of non-file: ${filePath}`);
return false;
}
fs_1.default.unlinkSync(filePath);
console.log(`ποΈ Removed: ${path_1.default.relative(process.cwd(), filePath)}`);
// After file removal, check if should delete parent folder (applicable inside API folder)
const parentDir = path_1.default.dirname(filePath);
// Define your API folder root path - adjust as per your CONFIG or project structure
const apiRoot = path_1.default.resolve(config_1.CONFIG.apiDir);
if (parentDir.startsWith(apiRoot)) {
// Check if folder is now empty
const entries = fs_1.default.readdirSync(parentDir);
if (entries.length === 0) {
// Remove the empty folder
try {
fs_1.default.rmdirSync(parentDir);
console.log(`ποΈ Removed empty directory: ${path_1.default.relative(process.cwd(), parentDir)}`);
}
catch (dirRemoveError) {
console.warn(`β οΈ Failed to remove directory ${parentDir}:`, dirRemoveError);
}
}
}
}
exports.generatedFiles.delete(filePath);
return true;
}
catch (error) {
console.error(`β Failed to remove file ${filePath}:`, error);
return false;
}
}
function safeRemoveDirectory(dirPath) {
try {
if (!fs_1.default.existsSync(dirPath))
return true;
const entries = fs_1.default.readdirSync(dirPath);
if (entries.length === 0) {
fs_1.default.rmdirSync(dirPath);
console.log(`ποΈ Removed empty directory: ${path_1.default.relative(process.cwd(), dirPath)}`);
return true;
}
return false;
}
catch (error) {
console.warn(`β οΈ Failed to remove directory ${dirPath}:`, error);
return false;
}
}
function markFileChanged(filePath) {
if (isProcessing) {
console.log(`βΈοΈ Queueing change for ${path_1.default.relative(process.cwd(), filePath)} (processing in progress)`);
}
exports.changedFiles.add(filePath);
cache_1.fileCache.delete(filePath); // Invalidate cache
// Remove from deleted files if it was marked as deleted
exports.deletedFiles.delete(filePath);
}
function markFileDeleted(filePath) {
exports.deletedFiles.add(filePath);
exports.changedFiles.delete(filePath);
cache_1.fileCache.delete(filePath);
console.log(`ποΈ Marked for deletion: ${path_1.default.relative(process.cwd(), filePath)}`);
}
function shouldRegenerateEverything() {
const now = Date.now();
const timeSinceLastScan = now - lastFullScan;
const hasChanges = exports.changedFiles.size > 0 || exports.deletedFiles.size > 0;
const cacheExpired = timeSinceLastScan > config_1.CONFIG.performance.cacheExpiryMs;
const tooManyChanges = exports.changedFiles.size > config_1.CONFIG.performance.maxFilesToProcess * 0.5;
// Always log details
console.log(`π Regeneration check: Changes present: ${hasChanges} (${exports.changedFiles.size} files), ` +
`Deleted files: ${exports.deletedFiles.size}, Cache expired: ${cacheExpired}, Too many changes: ${tooManyChanges}`);
if (hasChanges && exports.changedFiles.size > 0) {
const recentChanges = Array.from(exports.changedFiles)
.slice(0, 5)
.map(f => path_1.default.relative(process.cwd(), f))
.join(", ");
console.log(` Recent changed files: ${recentChanges}${exports.changedFiles.size > 5 ? '...' : ''}`);
}
if (exports.deletedFiles.size > 0) {
const deletedList = Array.from(exports.deletedFiles)
.slice(0, 3)
.map(f => path_1.default.relative(process.cwd(), f))
.join(", ");
console.log(` Recently deleted files: ${deletedList}${exports.deletedFiles.size > 3 ? '...' : ''}`);
}
// Only trigger full regeneration on cache expiry or too many changes
if (cacheExpired || tooManyChanges) {
console.log("π Triggering FULL regeneration due to cache expiry or too many changes");
return true; // full regeneration
}
// Return false to avoid full regeneration for ordinary changes
console.log("β‘ Skipping full regeneration; incremental regeneration can proceed");
return false;
}
function isObjectDestructured(param) {
return param.type.startsWith("{") && param.type.endsWith("}");
}
function generateFunctionSignature(func) {
// Filter out context parameters from client signature
const clientParams = func.parameters.filter(p => {
const type = p.type.toLowerCase();
return !(type.includes('quickwirecontext') ||
type.includes('context') && (type.includes('request') || type.includes('req')) ||
p.name.toLowerCase().includes('context') && p.optional);
});
if (clientParams.length === 0) {
return {
parameterType: "void",
callSignature: "() =>",
parameterUsage: "",
};
}
if (clientParams.length === 1) {
const param = clientParams[0];
if (isObjectDestructured(param)) {
return {
parameterType: param.type,
callSignature: `(${param.name}${param.optional ? "?" : ""}: ${param.type}) =>`,
parameterUsage: param.name,
};
}
const originalParamType = param.type;
const objectParamType = `{ ${param.name}${param.optional ? "?" : ""}: ${param.type} }`;
return {
parameterType: `${originalParamType} | ${objectParamType}`,
callSignature: `(${param.name}${param.optional ? "?" : ""}: ${originalParamType} | ${objectParamType}) =>`,
parameterUsage: `typeof ${param.name} === 'object' && '${param.name}' in ${param.name} ? ${param.name} : { ${param.name}: ${param.name} }`,
};
}
const paramStrings = clientParams.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`);
const objectType = `{ ${paramStrings.join("; ")} }`;
return {
parameterType: objectType,
callSignature: `(params: ${objectType}) =>`,
parameterUsage: "params",
};
}
function generateQuickwireFile(filePath, endpoints, moduleExports) {
try {
const relativePath = (0, utils_1.sanitizeFilePath)(path_1.default.relative(config_1.CONFIG.backendDir, filePath));
if (!moduleExports) {
moduleExports = (0, ast_1.analyzeModuleExports)(filePath);
}
if (moduleExports.functions.length === 0) {
console.log(`βοΈ Skipping ${relativePath} - no exported functions`);
return false;
}
const quickwireFilePath = path_1.default.join(config_1.CONFIG.quickwireDir, relativePath);
const lines = [
"// Auto-generated by Quickwire",
"// Do not edit manually - changes will be overwritten",
`// Generated from: ${relativePath}`,
`// Generated at: ${new Date().toISOString()}`,
"",
"type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;",
"",
"// Define proper request data types",
"type RequestData = FormData | Record<string, unknown> | string | number | boolean | null | undefined;",
"",
];
lines.push(`import { makeQuickwireRequest, convertToFormData } from "@/lib/utils.quickwire";`);
lines.push(`import type { AxiosRequestConfig } from "axios";`);
lines.push("");
lines.push("// Type-safe request helpers");
lines.push("type SafeRequestData<T> = T extends FormData ? FormData : T extends Record<string, unknown> ? T : RequestData;");
lines.push("");
const backendImportPath = `@/backend/${relativePath.replace(/\.[tj]s$/, "")}`;
const functionImports = moduleExports.functions
.map((func) => `${func.name} as ${func.name}Internal`)
.join(", ");
// Extract type names for import
const typeImports = moduleExports.types
.map((type) => type.name)
.join(", ");
// Combine function and type imports
const allImports = functionImports + (functionImports && typeImports ? ", " : "") + typeImports;
if (allImports) {
lines.push(`import { ${allImports} } from "${backendImportPath}";`);
lines.push("");
}
let functionsGenerated = 0;
moduleExports.functions.forEach((func) => {
const endpointInfo = endpoints[func.name];
if (!endpointInfo) {
console.warn(`β οΈ No endpoint info for function ${func.name} in ${relativePath}`);
return;
}
let route = endpointInfo.route;
const method = endpointInfo.method;
if (!route.startsWith("/api")) {
route = `/api${route}`;
}
function typeStringHasFile(typeStr) {
if (!typeStr)
return false;
const type = typeStr.toLowerCase();
return (type === "file" ||
type === "file[]" ||
type === "blob" ||
type === "blob[]" ||
type === "formdata" ||
type.includes("file") ||
type.includes("blob") ||
type.includes("formdata") ||
/\bfile\b/i.test(typeStr) ||
/\bblob\b/i.test(typeStr) ||
/\bformdata\b/i.test(typeStr));
}
// Filter out context parameters for client-side functions
const clientParams = func.parameters.filter(p => {
const type = p.type.toLowerCase();
const name = p.name.toLowerCase();
return !(type.includes('quickwirecontext') ||
type.includes('requestcontext') ||
type.includes('context') ||
name.includes('context') ||
name === 'ctx' ||
name === 'req' ||
name === 'request');
});
// Generate signature based only on client parameters
const clientSignature = clientParams.length > 0 ? generateFunctionSignature({
...func,
parameters: clientParams
}) : { parameterType: 'void', parameterUsage: undefined };
const hasFileParam = clientParams.some((p) => typeStringHasFile(p.type));
// Unified parameter handling for ALL methods - use body for everything
if (hasFileParam) {
// Handle file uploads with FormData
if (clientParams.length > 0) {
lines.push(`export const ${func.name} = (params: ${clientSignature.parameterType}, axiosConfig?: AxiosRequestConfig) => {`);
lines.push(` const formData: FormData = convertToFormData(params as Record<string, unknown>);`);
}
else {
lines.push(`export const ${func.name} = (axiosConfig?: AxiosRequestConfig) => {`);
lines.push(` const formData: FormData = new FormData();`);
}
lines.push(` return makeQuickwireRequest<UnwrapPromise<ReturnType<typeof ${func.name}Internal>>>(
\`${route}\`,
"${method}",
formData as SafeRequestData<FormData>,
axiosConfig
);`);
lines.push("};");
}
else {
// All other methods (including GET) - use JSON body
const allOptional = clientParams.length > 0 && clientParams.every((p) => p.optional);
const optionalModifier = allOptional && !clientSignature.parameterType.includes("|") ? "?" : "";
if (clientParams.length === 0) {
// No parameters
lines.push(`export const ${func.name} = (axiosConfig?: AxiosRequestConfig) => {`);
lines.push(` return makeQuickwireRequest<UnwrapPromise<ReturnType<typeof ${func.name}Internal>>>(
\`${route}\`,
"${method}",
undefined as SafeRequestData<void>,
axiosConfig
);`);
}
else if (clientParams.length === 1 && !isObjectDestructured(clientParams[0])) {
// Single parameter - wrap in object for consistent body structure
const param = clientParams[0];
lines.push(`export const ${func.name} = (${param.name}${optionalModifier}: ${param.type}, axiosConfig?: AxiosRequestConfig) => {`);
lines.push(` return makeQuickwireRequest<UnwrapPromise<ReturnType<typeof ${func.name}Internal>>>(
\`${route}\`,
"${method}",
${param.name} as unknown as RequestData,
axiosConfig
);`);
}
else {
// Multiple parameters or object destructured
lines.push(`export const ${func.name} = (params${optionalModifier}: ${clientSignature.parameterType}, axiosConfig?: AxiosRequestConfig) => {`);
lines.push(` return makeQuickwireRequest<UnwrapPromise<ReturnType<typeof ${func.name}Internal>>>(
\`${route}\`,
"${method}",
params as SafeRequestData<${clientSignature.parameterType}>,
axiosConfig
);`);
}
lines.push("};");
}
lines.push("");
functionsGenerated++;
});
const success = safeWriteFile(quickwireFilePath, lines.join("\n"), filePath);
if (success) {
console.log(`β
Generated quickwire file with ${functionsGenerated} functions: ${path_1.default.relative(process.cwd(), quickwireFilePath)}`);
}
return success;
}
catch (error) {
console.error(`β Failed to generate quickwire file for ${filePath}:`, error);
return false;
}
}
function generateApiRoutesForFile(filePath, moduleExports) {
const relativePath = (0, utils_1.sanitizeFilePath)(path_1.default.relative(config_1.CONFIG.backendDir, filePath));
if (!moduleExports) {
moduleExports = (0, ast_1.analyzeModuleExports)(filePath);
}
const endpoints = {};
if (moduleExports.functions.length === 0) {
return endpoints;
}
moduleExports.functions.forEach((func) => {
try {
const route = `/${relativePath.replace(/\.[tj]s$/, "")}/${(0, utils_1.pascalToKebab)(func.name)}`;
const method = func.httpMethod || "POST";
endpoints[func.name] = { route, method };
const apiFilePath = path_1.default.join(config_1.CONFIG.apiDir, route, config_1.CONFIG.apiRouteTemplate);
// Filter out context parameters to get only client parameters
const clientParams = func.parameters.filter(p => {
const type = p.type.toLowerCase();
const name = p.name.toLowerCase();
return !(type.includes('quickwirecontext') ||
type.includes('requestcontext') ||
type.includes('context') ||
name.includes('context') ||
name === 'ctx' ||
name === 'req' ||
name === 'request');
});
// Check if function has context parameter and find its position
const contextParamIndex = func.parameters.findIndex(p => {
const type = p.type.toLowerCase();
const name = p.name.toLowerCase();
return (type.includes('quickwirecontext') ||
type.includes('requestcontext') ||
type.includes('context') ||
name.includes('context') ||
name === 'ctx');
});
const hasContextParam = contextParamIndex !== -1;
let parameterHandling = "";
let functionCallParams = [];
const fileParams = clientParams.filter((p) => {
const type = p.type.toLowerCase();
return (type === "file" ||
type === "file[]" ||
type === "blob" ||
type === "blob[]" ||
type === "formdata" ||
type.includes("file") ||
type.includes("blob") ||
type.includes("formdata") ||
/\bfile\b/i.test(p.type) ||
/\bblob\b/i.test(p.type) ||
/\bformdata\b/i.test(p.type));
});
// Unified body parameter handling for ALL HTTP methods (including GET)
if (fileParams.length > 0) {
// File upload handling remains the same
parameterHandling = `
type ParsedValue = string | number | boolean | object | File;
function parseNestedValue(value: string): ParsedValue {
// Try to parse as JSON for nested objects/arrays
if (value.startsWith('{') || value.startsWith('[')) {
try {
return JSON.parse(value);
} catch {
return value;
}
}
// Try to parse as boolean
if (value === 'true') return true;
if (value === 'false') return false;
// Try to parse as number
if (!isNaN(Number(value)) && value !== '') {
return Number(value);
}
return value;
}
function assignDeep(obj: Record<string, ParsedValue | ParsedValue[]>, keyPath: string, value: ParsedValue): void {
const keys = keyPath.replace(/\\]/g, '').split(/[\\[.]/);
let current: Record<string, ParsedValue | ParsedValue[]> | ParsedValue[] = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!key) continue; // Skip empty keys from split
if (!(current as Record<string, ParsedValue | ParsedValue[]>)[key]) {
// Check if next key is a number (array index)
const nextKey = keys[i + 1];
(current as Record<string, ParsedValue | ParsedValue[]>)[key] = !isNaN(Number(nextKey)) ? [] : {};
}
current = (current as Record<string, ParsedValue | ParsedValue[]>)[key] as Record<string, ParsedValue | ParsedValue[]> | ParsedValue[];
}
const lastKey = keys[keys.length - 1];
if (lastKey) {
(current as Record<string, ParsedValue | ParsedValue[]>)[lastKey] = value instanceof File ? value : parseNestedValue(value.toString());
}
}
function formDataToObject(formData: FormData): Record<string, ParsedValue | ParsedValue[]> {
const obj: Record<string, ParsedValue | ParsedValue[]> = {};
for (const [key, value] of formData.entries()) {
assignDeep(obj, key, value as ParsedValue);
}
return obj;
}
let formData: FormData;
try {
const contentType = req.headers.get('content-type');
if (!contentType || !contentType.includes('multipart/form-data')) {
throw new Error('Expected multipart/form-data');
}
formData = await req.formData();
console.log('FormData entries:');
for (const [key, value] of formData.entries()) {
console.log(\` \${key}: \${value instanceof File ? \`File(\${value.name})\` : value}\`);
}
} catch (error) {
throw new Error(\`Invalid file upload: \${error instanceof Error ? error.message : 'Unknown error'}\`);
}
const params = formDataToObject(formData);
console.log('Parsed params:', JSON.stringify(params, (key, value) =>
value instanceof File ? \`File(\${value.name})\` : value, 2));
`;
// Build function call parameters with context in correct position
functionCallParams = [];
if (clientParams.length === 1 && isObjectDestructured(clientParams[0])) {
// Single object parameter
for (let i = 0; i < func.parameters.length; i++) {
if (i === contextParamIndex) {
functionCallParams.push("context");
}
else {
functionCallParams.push(`params as Parameters<typeof ${func.name}>[${i}]`);
}
}
}
else {
// Multiple parameters
let clientParamIndex = 0;
for (let i = 0; i < func.parameters.length; i++) {
if (i === contextParamIndex) {
functionCallParams.push("context");
}
else {
functionCallParams.push(`params.${clientParams[clientParamIndex].name} as Parameters<typeof ${func.name}>[${i}]`);
clientParamIndex++;
}
}
}
}
else {
// JSON body handling for ALL HTTP methods (including GET)
// Generate specific interface for the request body based on client parameters only
let bodyInterface = "";
if (clientParams.length === 0) {
// No client parameters
// bodyInterface = " interface RequestBody {\n [key: string]: never;\n }";
}
else if (clientParams.length === 1 && isObjectDestructured(clientParams[0])) {
// Single object parameter - use the exact type
// const param = clientParams[0];
// bodyInterface = ` type RequestBody = ${param.type};`;
}
else {
// Multiple parameters - create interface with exact parameter types
// const paramDeclarations = clientParams.map(p =>
// ` ${p.name}${p.optional ? "?" : ""}: ${p.type};`
// ).join("\n");
// bodyInterface = ` interface RequestBody {\n${paramDeclarations}\n }`;
}
parameterHandling = `
${bodyInterface}
let body;
try {
const contentType = req.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
body = await req.json();
} else {
const textBody = await req.text();
body = textBody ? JSON.parse(textBody) : {};
}
} catch (error) {
body = {};
}
`;
// Build function call parameters with context in correct position
functionCallParams = [];
if (clientParams.length === 0) {
// No client parameters, only context if it exists
if (hasContextParam) {
functionCallParams.push("context");
}
}
else if (clientParams.length === 1 && !isObjectDestructured(clientParams[0])) {
// Single parameter - extract from body object
let clientParamIndex = 0;
for (let i = 0; i < func.parameters.length; i++) {
if (i === contextParamIndex) {
functionCallParams.push("context");
}
else {
functionCallParams.push(`body${clientParams.length === 1 ? "" : "." + clientParams[clientParamIndex].name}`);
clientParamIndex++;
}
}
}
else if (clientParams.length === 1 && isObjectDestructured(clientParams[0])) {
// Single object parameter
for (let i = 0; i < func.parameters.length; i++) {
if (i === contextParamIndex) {
functionCallParams.push("context");
}
else {
functionCallParams.push("body");
}
}
}
else {
// Multiple parameters
let clientParamIndex = 0;
for (let i = 0; i < func.parameters.length; i++) {
if (i === contextParamIndex) {
functionCallParams.push("context");
}
else {
functionCallParams.push(`body.${clientParams.length === 1 ? "" : clientParams[clientParamIndex].name}`);
clientParamIndex++;
}
}
}
}
const functionCall = `${func.isAsync ? "await " : ""}${func.name}(${functionCallParams.map((param, index) => `${param} as unknown as Parameters<typeof ${func.name}>[${index}]`).join(", ")})`;
// Only generate context setup if the function has a QuickwireContext parameter
const contextSetup = hasContextParam ? `
// Setup request context for backend function
const context = {
req,
getHeaders: () => Object.fromEntries(req.headers.entries()),
getCookies: () => Object.fromEntries(
req.headers.get('cookie')
?.split(';')
?.map(c => c.trim().split('='))
?.filter(([k, v]) => k && v) || []
),
getIp: () => req.headers.get('x-forwarded-for')?.split(',')[0] ||
req.headers.get('x-real-ip') ||
'unknown',
getUserAgent: () => req.headers.get('user-agent') || 'unknown'
};
` : '';
// Extract types from module exports to import them
const typeImports = moduleExports.types.map(type => type.name).join(', ');
const importStatement = typeImports
? `import { ${func.name}, ${typeImports} } from "@/backend/${relativePath.replace(/\.[tj]s$/, "")}";
`
: `import { ${func.name} } from "@/backend/${relativePath.replace(/\.[tj]s$/, "")}";
`;
const handlerCode = `${importStatement}import { NextResponse } from "next/server";
${hasContextParam ? 'import type { QuickwireContext } from "quickwire/types";' : ''}
export async function ${method}(req: Request) {
try {
${parameterHandling}${contextSetup}
const result = ${functionCall};
return NextResponse.json(result ?? null);
} catch (error) {
console.error("API Error:", error);
const errorMessage = error instanceof Error ? error.message : "Internal server error";
return NextResponse.json(
{ error: errorMessage },
{ status: 500 }
);
}
}
`;
const success = safeWriteFile(apiFilePath, handlerCode, filePath);
if (!success) {
console.error(`β Failed to generate API route for ${func.name}`);
}
}
catch (error) {
console.error(`β Error generating API route for ${func.name}:`, error);
}
});
return endpoints;
}
// ... [Rest of the API documentation functions remain the same] ...
function cleanupOrphanedFiles() {
console.log("π§Ή Starting cleanup of orphaned files...");
const cleanupDirs = [config_1.CONFIG.quickwireDir, config_1.CONFIG.apiDir];
let removedCount = 0;
for (const dir of cleanupDirs) {
if (!fs_1.default.existsSync(dir))
continue;
try {
removedCount += walkAndCleanup(dir);
}
catch (error) {
console.warn(`β οΈ Failed to cleanup directory ${dir}:`, error);
}
}
console.log(`π§Ή Cleanup completed: removed ${removedCount} orphaned files`);
}
function walkAndCleanup(dir) {
let removedCount = 0;
try {
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path_1.default.join(dir, entry.name);
if (entry.isDirectory()) {
removedCount += walkAndCleanup(fullPath);
// Try to remove empty directory
try {
const isEmpty = fs_1.default.readdirSync(fullPath).length === 0;
if (isEmpty && safeRemoveDirectory(fullPath)) {
// Directory was removed, don't count in removedCount as it's not a file
}
}
catch {
// Directory not empty or other error, ignore
}
}
else if (entry.isFile()) {
// Only remove TypeScript/JavaScript files that we didn't generate
if ((fullPath.endsWith(".ts") || fullPath.endsWith(".js")) &&
!exports.generatedFiles.has(fullPath)) {
// Additional safety: check if file contains generated marker
try {
const content = fs_1.default.readFileSync(fullPath, 'utf8');
if (content.includes('Auto-generated by Quickwire')) {
if (safeRemoveFile(fullPath)) {
removedCount++;
}
}
}
catch (error) {
console.warn(`β οΈ Could not read file for cleanup check ${fullPath}:`, error);
}
}
}
}
}
catch (error) {
console.warn(`β οΈ Error during cleanup walk in ${dir}:`, error);
}
return removedCount;
}
function processBackendFile(filePath) {
if (!(0, utils_1.shouldProcessFile)(filePath, config_1.CONFIG) || !fs_1.default.existsSync(filePath)) {
return false;
}
try {
// Remove all old generated files for this backend file before generating new ones
const oldGeneratedFiles = exports.backendToGenerated.get(filePath);
if (oldGeneratedFiles) {
console.log(`π§Ή Cleaning up ${oldGeneratedFiles.size} old generated files for ${path_1.default.relative(process.cwd(), filePath)}`);
for (const oldFile of oldGeneratedFiles) {
if (safeRemoveFile(oldFile)) {
console.log(`ποΈ Removed orphaned generated file: ${path_1.default.relative(process.cwd(), oldFile)}`);
}
}
exports.backendToGenerated.delete(filePath);
}
// Analyze current exports
const moduleExports = (0, ast_1.analyzeModuleExports)(filePath);
if (moduleExports.functions.length === 0) {
console.log(`βοΈ No exported functions found in ${path_1.default.relative(process.cwd(), filePath)} - skipping generation`);
return false;
}
// Generate API routes and Quickwire files for existing exports
const endpoints = generateApiRoutesForFile(filePath, moduleExports);
const quickwireSuccess = generateQuickwireFile(filePath, endpoints, moduleExports);
if (!quickwireSuccess) {
console.warn(`β οΈ Failed to generate quickwire client file for ${path_1.default.relative(process.cwd(), filePath)}`);
return false;
}
// Track newly generated files for this backend file
const newGeneratedFiles = new Set();
for (const [generatedFilePath, metadata] of exports.generatedFiles) {
if (metadata.sourceFile === filePath) {
newGeneratedFiles.add(generatedFilePath);
}
}
if (newGeneratedFiles.size > 0) {
exports.backendToGenerated.set(filePath, newGeneratedFiles);
console.log(`π Generated ${newGeneratedFiles.size} files for ${path_1.default.relative(process.cwd(), filePath)}`);
}
else {
exports.backendToGenerated.delete(filePath);
console.log(`π§Ή No files generated; cleared tracking for ${path_1.default.relative(process.cwd(), filePath)}`);
}
return true;
}
catch (error) {
console.error(`β Error processing ${path_1.default.relative(process.cwd(), filePath)}:`, error);
return false;
}
}
async function scanAllBackendFunctions() {
if (isProcessing) {
console.log("βΈοΈ Scan already in progress, skipping...");
return;
}
isProcessing = true;
try {
const now = Date.now();
const isFullRegen = shouldRegenerateEverything();
const hasChanges = exports.changedFiles.size > 0 || exports.deletedFiles.size > 0;
if (!hasChanges && !isFullRegen) {
console.log("β‘ No changes detected, skipping regeneration");
return;
}
if (isFullRegen) {
console.log("π Performing full regeneration...");
// Clear tracking maps but keep file metadata for cleanup
const oldGenerated = new Map(exports.generatedFiles);
exports.generatedFiles.clear();
exports.backendToGenerated.clear();
if (now - lastFullScan > config_1.CONFIG.performance.cacheExpiryMs) {
cache_1.fileCache.clear();
console.log("ποΈ Cleared file cache due to expiry");
}
}
console.log("π Scanning backend functions...");
const startTime = Date.now();
// Process deleted files first
for (const deletedPath of exports.deletedFiles) {
const genSet = exports.backendToGenerated.get(deletedPath);
if (genSet) {
console.log(`ποΈ Cleaning up ${genSet.size} files for deleted source: ${path_1.default.relative(process.cwd(), deletedPath)}`);
for (const f of genSet) {
safeRemoveFile(f);
}
exports.backendToGenerated.delete(deletedPath);
}
cache_1.fileCache.delete(deletedPath);
}
exports.deletedFiles.clear();
let processedCount = 0;
let skippedCount = 0;
let errorCount = 0;
async function walk(dir) {
if (!fs_1.default.existsSync(dir))
return;
let entries;
try {
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
}
catch (error) {
console.warn(`β οΈ Failed to read directory ${dir}:`, error);
return;
}
for (const entry of entries) {
const filePath = path_1.default.join(dir, entry.name);
if (processedCount + skippedCount + errorCount > config_1.CONFIG.performance.maxFilesToProcess) {
console.warn(`β οΈ Reached maximum file limit (${config_1.CONFIG.performance.maxFilesToProcess}), stopping scan`);
return;
}
if (entry.isDirectory()) {
if (!["node_modules", ".git", ".next", "dist", "build"].includes(entry.name)) {
await walk(filePath);
}
}
else if (entry.isFile()) {
// Check if we need to process this file
if (!isFullRegen && !exports.changedFiles.has(filePath)) {
skippedCount++;
continue;
}
try {
console.log(`π§ Processing: ${path_1.default.relative(process.cwd(), filePath)}`);
if (processBackendFile(filePath)) {
processedCount++;
}
else {
skippedCount++;
}
}
catch (error) {
console.error(`β Error processing ${filePath}:`, error);
errorCount++;
}
}
}
}
await walk(config_1.CONFIG.backendDir);
// Generate documentation if enabled
if (config_1.CONFIG.performance.enableDocGeneration && (isFullRegen || hasChanges)) {
try {
generateDocumentationFiles();
}
catch (error) {
console.error("β Failed to generate documentation:", error);
}
}
// Cleanup orphaned files
cleanupOrphanedFiles();
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`β
Scan completed: ${processedCount} processed, ${skippedCount} skipped, ${errorCount} errors (${duration}ms)`);
if (isFullRegen) {
lastFullScan = now;
}
exports.changedFiles.clear();
// Performance summary
if (processedCount > 0) {
console.log("π Performance Summary:");
console.log(` Generated files: ${exports.generatedFiles.size}`);
console.log(` Cache entries: ${cache_1.fileCache.size}`);
console.log(` Processing time: ${duration}ms`);
console.log(` Avg per file: ${Math.round(duration / Math.max(processedCount, 1))}ms`);
// Count HTTP methods
const methodCounts = {};
for (const [filePath] of exports.generatedFiles) {
if (filePath.includes("/api/") && !filePath.includes("quickwire-docs")) {
try {
const content = fs_1.default.readFileSync(filePath, "utf-8");
const methodMatch = content.match(/export async function (GET|POST|PUT|PATCH|DELETE)/);
if (methodMatch) {
const method = methodMatch[1];
methodCounts[method] = (methodCounts[method] || 0) + 1;
}
}
catch {
// Ignore read errors
}
}
}
if (Object.keys(methodCounts).length > 0) {
console.log("π HTTP Method Distribution:");
Object.entries(methodCounts)
.sort(([, a], [, b]) => b - a)
.forEach(([method, count]) => {
console.log(` ${method}: ${count} endpoints`);
});
}
}
}
catch (error) {
console.error("β Fatal error during scan:", error);
}
finally {
isProcessing = false;
}
}
// Utility function to get generation statistics
function getGenerationStats() {
const bySourceFile = {};
let oldestFile = null;
let newestFile = null;
const now = Date.now();
for (const [filePath, metadata] of exports.generatedFiles) {
const sourceRelative = path_1.default.relative(process.cwd(), metadata.sourceFile);
bySourceFile[sourceRelative] = (bySourceFile[sourceRelative] || 0) + 1;
const age = now - metadata.generatedAt;
if (!oldestFile || age > oldestFile.age) {
oldestFile = { path: path_1.default.relative(process.cwd(), filePath), age };
}
if (!newestFile || age < newestFile.age) {
newestFile = { path: path_1.default.relative(process.cwd(), filePath), age };
}
}
return {
totalGenerated: exports.generatedFiles.size,
bySourceFile,
oldestFile,
newestFile,
};
}
function generateParameterSchema(parameters) {
if (parameters.length === 0) {
return null;
}
if (parameters.length === 1) {
const param = parameters[0];
if (isObjectDestructured(param)) {
return parseTypeToSchema(param.type);
}
const directSchema = parseTypeToSchema(param.type);
const wrappedSchema = {
type: "object",
properties: {
[param.name]: directSchema,
},
required: param.optional ? [] : [param.name],
};
return {
oneOf: [directSchema, wrappedSchema],
};
}
const properties = {};
const required = [];
parameters.forEach((param) => {
properties[param.name] = parseTypeToSchema(param.type);
if (!param.optional) {
required.push(param.name);
}
});
return {
type: "object",
properties,
required: required.length > 0 ? required : undefined,
};
}
function parseTypeToSchema(typeStr) {
const type = typeStr.trim();
// Basic types
if (type === "string")
return { type: "string" };
if (type === "number")
return { type: "number" };
if (type === "boolean")
return { type: "boolean" };
if (type === "any")
return {};
if (type === "unknown")
return {};
if (type === "void")
return { type: "null" };
if (type === "File")
return { type: "string", format: "binary" };
if (type === "Blob")
return { type: "string", format: "binary" };
if (type === "FormData")
return { type: "object" };
// Array types
if (type.endsWith("[]")) {
const itemType = type.slice(0, -2);
return {
type: "array",
items: parseTypeToSchema(itemType),
};
}
const arrayMatch = type.match(/^Array<(.+)>$/);
if (arrayMatch) {
return {
type: "array",
items: parseTypeToSchema(arrayMatch[1]),
};
}
// Promise types
const promiseMatch = type.match(/^Promise<(.+)>$/);
if (promiseMatch) {
return parseTypeToSchema(promiseMatch[1]);
}
// Union types
if (type.includes("|")) {
const unionTypes = type.split("|").map((t) => t.trim());
return {
oneOf: unionTypes.map((t) => parseTypeToSchema(t)),
};
}
// Object types
if (type.startsWith("{") && type.endsWith("}")) {
const properties = {};
const required = [];
const content = type.slice(1, -1).trim();
if (content) {
const props = splitObjectProperties(content);
props.forEach((prop) => {
const colonIndex = prop.indexOf(":");
if (colonIndex > 0) {
let key = prop.substring(0, colonIndex).trim();
const optional = key.endsWith("?");
if (optional) {
key = key.slice(0, -1).trim();
}
const valueType = prop.substring(colonIndex + 1).trim();
properties[key] = parseTypeToSchema(valueType);
if (!optional) {
required.push(key);
}
}
});
}
return {
type: "object",
properties,
required: required.length > 0 ? required : undefined,
};
}
// Custom/unknown types
return {
type: "object",
description: `Custom type: ${type}`,
};
}
function splitObjectProperties(content) {
const props = [];
let current = "";
let braceCount = 0;
let inString = false;
let stringChar = "";
for (let i = 0; i < content.length; i++) {
const char = content[i];
if (!inString && (char === '"' || char === "'")) {
inString = true;
stringChar = char;
}
else if (inString && char === stringChar && content[i - 1] !== "\\") {
inString = false;
}
else if (!inString) {
if (char === "{")
braceCount++;
else if (char === "}")
braceCount--;
else if ((char === ";" || char === ",") && braceCount === 0) {
if (current.trim())
props.push(current.trim());
current = "";
continue;
}
}
current += char;
}
if (current.trim())
props.push(current.trim());
return props;
}
function generateFunctionTags(filePath, functionName) {
const relativePath = (0, utils_1.sanitizeFilePath)(path_1.default.relative(config_1.CONFIG.backendDir, filePath));
const pathParts = relativePath.replace(/\.[tj]s$/, "").split("/");
return pathParts.map((part) => part.charAt(0).toUpperCase() + part.slice(1));
}
function generateFunctionSummary(functionName, httpMethod) {
const methodActions = {
GET: "Retrieve",
POST: "Create or process",
PUT: "Update or replace",
PATCH: "Partially update",
DELETE: "Delete or remove",
};
const action = methodActions[httpMethod] || "Execute";
const readableName = functionName
.replace(/([A-Z])/g, " $1")
.toLowerCase()
.trim();
return `${action} ${readableName}`;
}
function generateApiDocumentation() {
const docs = [];
function walkDir(dir) {
if (!fs_1.default.existsSync(dir))
return;
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const filePath = path_1.default.join(dir, entry.name);
if (entry.isDirectory()) {
if (!["node_modules", ".git", ".next", "dist", "build"].includes(entry.name)) {
walkDir(filePath);
}
}
else if (entry.isFile() && (0, utils_1.shouldProcessFile)(filePath, config_1.CONFIG)) {
try {
const moduleExports = (0, ast_1.analyzeModuleExports)(filePath);
const relat