stack/apps/backend/scripts/generate-openapi-fumadocs.ts
BilalG1 609579abab
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482)
2026-05-29 15:21:59 -07:00

105 lines
4.1 KiB
TypeScript

import { parseOpenAPI, parseWebhookOpenAPI } from '@/lib/openapi';
import { isSmartRouteHandler } from '@/route-handlers/smart-route-handler';
import { webhookEvents } from '@hexclave/shared/dist/interface/webhooks';
import { writeFileSyncIfChanged } from '@hexclave/shared/dist/utils/fs';
import { HTTP_METHODS } from '@hexclave/shared/dist/utils/http';
import { typedKeys } from '@hexclave/shared/dist/utils/objects';
import { stringCompare } from '@hexclave/shared/dist/utils/strings';
import fs from 'fs';
import { glob } from 'glob';
import path from 'path';
async function main() {
console.log("Started Fumadocs OpenAPI schema generator");
// Create openapi directory in Fumadocs project
const fumaDocsOpenApiDir = path.resolve("../../docs/openapi");
const mintlifyOpenApiDir = path.resolve("../../docs-mintlify/openapi");
// Ensure the openapi directory exists
if (!fs.existsSync(fumaDocsOpenApiDir)) {
console.log('Creating OpenAPI directory...');
fs.mkdirSync(fumaDocsOpenApiDir, { recursive: true });
}
if (!fs.existsSync(mintlifyOpenApiDir)) {
console.log('Creating Mintlify OpenAPI directory...');
fs.mkdirSync(mintlifyOpenApiDir, { recursive: true });
}
// Generate OpenAPI specs for each audience (let parseOpenAPI handle the filtering)
const filePathPrefix = path.resolve(process.platform === "win32" ? "apps/src/app/api/latest" : "src/app/api/latest");
const importPathPrefix = "@/app/api/latest";
const filePaths = [...await glob(filePathPrefix + "/**/route.{js,jsx,ts,tsx}")].sort((a, b) => stringCompare(a, b));
const endpoints = new Map(await Promise.all(filePaths.map(async (filePath) => {
if (!filePath.startsWith(filePathPrefix)) {
throw new Error(`Invalid file path: ${filePath}`);
}
const suffix = filePath.slice(filePathPrefix.length);
const midfix = suffix.slice(0, suffix.lastIndexOf("/route."));
const importPath = `${importPathPrefix}${suffix}`;
const urlPathRaw = midfix.replaceAll("[", "{").replaceAll("]", "}").replaceAll(/\/\(.*\)/g, "");
// OpenAPI path keys must not be empty (Mintlify and other tooling reject `""`).
const urlPath = urlPathRaw === "" ? "/" : urlPathRaw;
const myModule = await import(importPath);
const handlersByMethod = new Map(
typedKeys(HTTP_METHODS).map(method => [method, myModule[method]] as const)
.filter(([_, handler]) => isSmartRouteHandler(handler))
);
return [urlPath, handlersByMethod] as const;
})));
console.log(`Found ${endpoints.size} total endpoint files`);
// Generate specs for each audience using parseOpenAPI's built-in filtering
for (const audience of ['client', 'server', 'admin'] as const) {
const openApiSchemaObject = parseOpenAPI({
endpoints,
audience, // Let parseOpenAPI handle the audience-specific filtering
});
// Update server URL for Fumadocs
openApiSchemaObject.servers = [{
url: 'https://api.hexclave.com/api/v1',
description: 'Hexclave REST API',
}];
console.log(`Generated ${Object.keys(openApiSchemaObject.paths || {}).length} endpoints for ${audience} audience`);
const audienceJson = JSON.stringify(openApiSchemaObject, null, 2);
// Write JSON files for Fumadocs (they prefer JSON over YAML)
writeFileSyncIfChanged(
path.join(fumaDocsOpenApiDir, `${audience}.json`),
audienceJson
);
writeFileSyncIfChanged(
path.join(mintlifyOpenApiDir, `${audience}.json`),
audienceJson
);
}
// Generate webhooks schema
const webhookOpenAPISchema = parseWebhookOpenAPI({
webhooks: webhookEvents,
});
const webhooksJson = JSON.stringify(webhookOpenAPISchema, null, 2);
writeFileSyncIfChanged(
path.join(fumaDocsOpenApiDir, 'webhooks.json'),
webhooksJson
);
writeFileSyncIfChanged(
path.join(mintlifyOpenApiDir, 'webhooks.json'),
webhooksJson
);
console.log("Successfully updated Fumadocs OpenAPI schemas with proper audience filtering");
}
// eslint-disable-next-line no-restricted-syntax
main().catch((...args) => {
console.error(`ERROR! Could not update Fumadocs OpenAPI schema`, ...args);
process.exit(1);
});