@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
JavaScript
;
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);
}