UNPKG

@sasonarik/nextapi-swagger

Version:

CLI tool to generate Next.js API routes and types from Swagger/OpenAPI specs

146 lines (142 loc) 5.91 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateClient = generateClient; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const helper_1 = require("../helper/helper"); const chalk_1 = __importDefault(require("chalk")); async function generateClient(spec, srcRoot) { const outPath = path_1.default.join(srcRoot, "utils", "apiRequests.ts"); const schemas = spec.components?.schemas ?? spec.definitions ?? {}; const names = []; for (const [rawName] of Object.entries(schemas)) { names.push((0, helper_1.cleanTypeName)(rawName)); } const imports = [ `"use server";`, `import { ApiResult, ${names.join(", ")} } from '@/types/api';`, `const nextUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:3000';`, `const token = typeof window !== 'undefined' ? localStorage.getItem('token') ?? '' : '';`, ]; const sharedTypesAndFunction = ` interface GlobalApiReq<V> { method: "GET" | "POST" | "PUT" | "DELETE"; url: string; requestData: V | null; } export async function globalRequest<T>( data: GlobalApiReq<T> ): Promise<ApiResult<T | null>> { try { const response = await fetch(nextUrl + data.url, { method: data.method, headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, ...(data.method === "GET" || data.requestData == null ? {} : { body: JSON.stringify(data.requestData) }), }); if (response.status === 401) { return { data: null, message: "No valid JWT token has been provided.", status: false, }; } return await response.json(); } catch (error) { console.error("API request failed:", error); return { data: null, message: "API request failed", status: false }; } } `.trim(); const lines = []; for (const [route, methods] of Object.entries(spec.paths || {})) { for (const [method, operation] of Object.entries(methods ?? {})) { if (!operation || typeof operation !== "object") continue; const op = operation; const methodUpper = method.toUpperCase(); const name = op.operationId || `${method}${route.replace(/[^a-zA-Z]/g, "")}`; const summary = op.summary ? `// ${op.summary}` : ""; const pathParams = (op.parameters || []) .filter(isParameterObject) .filter((p) => p.in === "path"); let urlTemplate = route; let functionArgs = []; // Add path parameters to function arguments for (const param of pathParams) { functionArgs.push(`${param.name}: string`); urlTemplate = urlTemplate.replace(`{${param.name}}`, `\${${param.name}}`); } let inputType = "any"; let hasBody = false; if (op.requestBody && "content" in op.requestBody) { const json = op.requestBody.content?.["application/json"]; const schema = json?.schema; if (schema) { inputType = (0, helper_1.resolveType)(schema); hasBody = true; } } if (hasBody) { functionArgs.push(`data: ${inputType}`); } const returnType = "ApiResult<any>"; // You can enhance this by resolving response schema too const functionCode = ` ${summary} export async function ${name}(${functionArgs.join(", ")}): Promise<${returnType}> { return await globalRequest<any>({ method: "${methodUpper}", url: \`${urlTemplate}\`, requestData: ${hasBody ? "data" : "null"}, }); } `.trim(); lines.push(functionCode); } } let existingContent = ""; if (await fs_extra_1.default.pathExists(outPath)) { existingContent = await fs_extra_1.default.readFile(outPath, "utf8"); console.log(chalk_1.default.yellow(`ℹ️ Existing file found at ${outPath}`)); } // Split existing content by exported functions (simple split by `export async function`) // We'll store the whole function blocks to compare exact matches. const existingFunctions = new Set(); if (existingContent) { // Simple regex to extract exported async functions (non-greedy) const functionRegex = /export async function [\s\S]+?}\n?/g; const matches = existingContent.match(functionRegex) ?? []; for (const fn of matches) { existingFunctions.add(fn.trim()); } } // Filter lines to include only new functions not already in the file exactly const uniqueNewFunctions = lines.filter((fn) => !existingFunctions.has(fn)); if (uniqueNewFunctions.length === 0) { console.log(chalk_1.default.gray("ℹ️ No new functions to add.")); return; } const newContent = imports .concat("", sharedTypesAndFunction, "", ...uniqueNewFunctions) .join("\n\n"); // Combine existing content and new unique functions // You might want to place imports/shared function only once, so only add them if existing content is empty const fullFile = existingContent.trim() === "" ? newContent : existingContent.trim() + "\n\n" + uniqueNewFunctions.join("\n\n"); await fs_extra_1.default.ensureDir(path_1.default.dirname(outPath)); await fs_extra_1.default.writeFile(outPath, fullFile); console.log(chalk_1.default.greenBright(`✅ Client written to ${outPath}`)); } function isParameterObject(param) { return !("$ref" in param); }