next-openapi-gen 0.0.3 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ import fs from "fs";
2
+ import fse from "fs-extra";
3
+ import path from "path";
4
+ import ora from "ora";
5
+ import { OpenApiGenerator } from "../lib/openapi-generator.js";
6
+ export async function generate() {
7
+ const spinner = ora("Generating OpenAPI specification...\n").start();
8
+ const generator = new OpenApiGenerator();
9
+ const apiDocs = generator.generate();
10
+ const config = generator.getConfig();
11
+ // Check if public dir exists
12
+ const outputDir = path.resolve("./public");
13
+ await fse.ensureDir(outputDir);
14
+ // Write api docs
15
+ const outputFile = path.join(outputDir, config.outputFile);
16
+ fs.writeFileSync(outputFile, JSON.stringify(apiDocs, null, 2));
17
+ spinner.succeed(`OpenAPI specification generated at ${outputFile}`);
18
+ }
@@ -4,27 +4,9 @@ import fs from "fs";
4
4
  import ora from "ora";
5
5
  import { exec } from "child_process";
6
6
  import util from "util";
7
+ import openapiTemplate from "../openapi-template.js";
7
8
  const execPromise = util.promisify(exec);
8
9
  const spinner = ora("Initializing project with OpenAPI template...\n");
9
- const openApiTemplate = {
10
- openapi: "3.0.0",
11
- info: {
12
- title: "API Documentation",
13
- version: "1.0.0",
14
- description: "This is the OpenAPI specification for your project.",
15
- },
16
- servers: [
17
- {
18
- url: "http://localhost:3000",
19
- description: "Local development server",
20
- },
21
- ],
22
- paths: {},
23
- apiPath: "./src/app/api",
24
- docsUrl: "api-docs",
25
- ui: "swagger",
26
- outputPath: "./public/swagger.json",
27
- };
28
10
  const getPackageManager = async () => {
29
11
  if (fs.existsSync(path.join(process.cwd(), "yarn.lock"))) {
30
12
  return "yarn";
@@ -75,12 +57,14 @@ function extendOpenApiTemplate(spec, options) {
75
57
  spec.ui = options.ui ?? spec.ui;
76
58
  spec.docsUrl = options.docsUrl ?? spec.docsUrl;
77
59
  }
78
- export async function init(ui, docsUrl) {
60
+ export async function init(options) {
61
+ const { ui, docsUrl } = options;
79
62
  spinner.start();
80
63
  try {
81
64
  const outputPath = path.join(process.cwd(), "next.openapi.json");
82
- extendOpenApiTemplate(openApiTemplate, { docsUrl, ui });
83
- await fse.writeJson(outputPath, openApiTemplate, { spaces: 2 });
65
+ const template = { ...openapiTemplate };
66
+ extendOpenApiTemplate(template, { docsUrl, ui });
67
+ await fse.writeJson(outputPath, template, { spaces: 2 });
84
68
  spinner.succeed(`Created OpenAPI template in next.openapi.json`);
85
69
  if (ui === "swagger") {
86
70
  createDocsPage();
package/dist/index.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
2
+ import { Command, Option } from "commander";
3
3
  import { init } from "./commands/init.js";
4
- import { generateOpenapiSpec } from "./commands/generate-openapi-spec.js";
4
+ import { generate } from "./commands/generate.js";
5
5
  const program = new Command();
6
6
  program
7
7
  .name("next-openapi-gen")
8
8
  .version("0.0.1")
9
9
  .description("Super fast and easy way to generate OpenAPI documentation for Next.js");
10
10
  program
11
- .command("init <ui> <docs-url>")
11
+ .command("init")
12
+ .addOption(new Option("-i, --ui <type>", "Specify the UI type, e.g., swagger")
13
+ .choices(["swagger", "element", "redoc"])
14
+ .default("swagger"))
15
+ .option("-u, --docs-url <url>", "Specify the docs URL", "api-docs")
12
16
  .description("Initialize a openapi specification")
13
17
  .action(init);
14
18
  program
15
19
  .command("generate")
16
20
  .description("Generate a specification based on api routes")
17
- .action(generateOpenapiSpec);
21
+ .action(generate);
18
22
  program.parse(process.argv);
@@ -0,0 +1,34 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { RouteProcessor } from "./route-processor.js";
4
+ import { cleanSpec } from "./utils.js";
5
+ export class OpenApiGenerator {
6
+ config;
7
+ template;
8
+ routeProcessor;
9
+ constructor() {
10
+ const templatePath = path.resolve("./next.openapi.json");
11
+ this.template = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
12
+ this.config = this.getConfig();
13
+ this.routeProcessor = new RouteProcessor(this.config);
14
+ }
15
+ getConfig() {
16
+ // @ts-ignore
17
+ const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes } = this.template;
18
+ return {
19
+ apiDir,
20
+ schemaDir,
21
+ docsUrl,
22
+ ui,
23
+ outputFile,
24
+ includeOpenApiRoutes,
25
+ };
26
+ }
27
+ generate() {
28
+ const { apiDir } = this.config;
29
+ this.routeProcessor.scanApiRoutes(apiDir);
30
+ this.template.paths = this.routeProcessor.getSwaggerPaths();
31
+ const openapiSpec = cleanSpec(this.template);
32
+ return openapiSpec;
33
+ }
34
+ }
@@ -0,0 +1,135 @@
1
+ import * as t from "@babel/types";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import traverse from "@babel/traverse";
5
+ import { parse } from "@babel/parser";
6
+ import { SchemaProcessor } from "./schema-processor.js";
7
+ import { capitalize, extractJSDocComments, getOperationId } from "./utils.js";
8
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
9
+ const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
10
+ export class RouteProcessor {
11
+ swaggerPaths = {};
12
+ schemaProcessor;
13
+ config;
14
+ constructor(config) {
15
+ this.config = config;
16
+ this.schemaProcessor = new SchemaProcessor(config.schemaDir);
17
+ }
18
+ isRoute(varName) {
19
+ return HTTP_METHODS.includes(varName);
20
+ }
21
+ processFile(filePath) {
22
+ const content = fs.readFileSync(filePath, "utf-8");
23
+ const ast = parse(content, {
24
+ sourceType: "module",
25
+ plugins: ["typescript"],
26
+ });
27
+ traverse.default(ast, {
28
+ ExportNamedDeclaration: (path) => {
29
+ const declaration = path.node.declaration;
30
+ if (t.isFunctionDeclaration(declaration) &&
31
+ t.isIdentifier(declaration.id)) {
32
+ const dataTypes = extractJSDocComments(path);
33
+ if (this.isRoute(declaration.id.name)) {
34
+ this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
35
+ }
36
+ }
37
+ if (t.isVariableDeclaration(declaration)) {
38
+ declaration.declarations.forEach((decl) => {
39
+ if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
40
+ if (this.isRoute(decl.id.name)) {
41
+ this.addRouteToPaths(decl.id.name, filePath, extractJSDocComments(path));
42
+ }
43
+ }
44
+ });
45
+ }
46
+ },
47
+ });
48
+ }
49
+ scanApiRoutes(dir) {
50
+ const files = fs.readdirSync(dir);
51
+ files.forEach((file) => {
52
+ const filePath = path.join(dir, file);
53
+ const stat = fs.statSync(filePath);
54
+ if (stat.isDirectory()) {
55
+ this.scanApiRoutes(filePath);
56
+ // @ts-ignore
57
+ }
58
+ else if (file.endsWith(".ts")) {
59
+ this.processFile(filePath);
60
+ }
61
+ });
62
+ }
63
+ addRouteToPaths(varName, filePath, dataTypes) {
64
+ const method = varName.toLowerCase();
65
+ const routePath = this.getRoutePath(filePath);
66
+ const rootPath = capitalize(routePath.split("/")[1]);
67
+ const operationId = getOperationId(routePath, method);
68
+ const { summary, description, isOpenApi } = dataTypes;
69
+ if (this.config.includeOpenApiRoutes && !isOpenApi) {
70
+ // If flag is enabled and there is no @openapi tag, then skip path
71
+ return;
72
+ }
73
+ if (!this.swaggerPaths[routePath]) {
74
+ this.swaggerPaths[routePath] = {};
75
+ }
76
+ const { params, body, responses } = this.schemaProcessor.getSchemaContent(dataTypes);
77
+ const definition = {
78
+ operationId: operationId,
79
+ summary: summary,
80
+ description: description,
81
+ tags: [rootPath],
82
+ parameters: params,
83
+ };
84
+ // Add request body
85
+ if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
86
+ definition.requestBody =
87
+ this.schemaProcessor.createRequestBodySchema(body);
88
+ }
89
+ // Add responses
90
+ definition.responses = responses
91
+ ? this.schemaProcessor.createResponseSchema(responses)
92
+ : {};
93
+ this.swaggerPaths[routePath][method] = definition;
94
+ }
95
+ getRoutePath(filePath) {
96
+ const suffixPath = filePath.split("api")[1];
97
+ return suffixPath
98
+ .replace("route.ts", "")
99
+ .replaceAll("\\", "/")
100
+ .replace(/\/$/, "");
101
+ }
102
+ getSortedPaths(paths) {
103
+ function comparePaths(a, b) {
104
+ const aMethods = this.swaggerPaths[a] || {};
105
+ const bMethods = this.swaggerPaths[b] || {};
106
+ // Extract tags for all methods in path a
107
+ const aTags = Object.values(aMethods).flatMap((method) => method.tags || []);
108
+ // Extract tags for all methods in path b
109
+ const bTags = Object.values(bMethods).flatMap((method) => method.tags || []);
110
+ // Assume we are interested in only the first tags
111
+ const aPrimaryTag = aTags[0] || "";
112
+ const bPrimaryTag = bTags[0] || "";
113
+ // Sort alphabetically based on the first tag
114
+ const tagComparison = aPrimaryTag.localeCompare(bPrimaryTag);
115
+ if (tagComparison !== 0) {
116
+ return tagComparison; // Return the result of tag comparison
117
+ }
118
+ // Compare lengths of the paths
119
+ const aLength = a.split("/").length;
120
+ const bLength = b.split("/").length;
121
+ // Return the result of length comparison
122
+ return aLength - bLength; // Shorter paths come before longer ones
123
+ }
124
+ return Object.keys(paths)
125
+ .sort(comparePaths.bind(this))
126
+ .reduce((sorted, key) => {
127
+ sorted[key] = paths[key];
128
+ return sorted;
129
+ }, {});
130
+ }
131
+ getSwaggerPaths() {
132
+ const paths = this.getSortedPaths(this.swaggerPaths);
133
+ return this.getSortedPaths(paths);
134
+ }
135
+ }
@@ -0,0 +1,155 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parse } from "@babel/parser";
4
+ import traverse from "@babel/traverse";
5
+ import * as t from "@babel/types";
6
+ export class SchemaProcessor {
7
+ schemaDir;
8
+ constructor(schemaDir) {
9
+ this.schemaDir = path.resolve(schemaDir);
10
+ }
11
+ findSchemaDefinition(schemaName) {
12
+ let schemaNode = null;
13
+ this.scanSchemaDir(this.schemaDir, schemaName, (node) => {
14
+ schemaNode = node;
15
+ });
16
+ return schemaNode;
17
+ }
18
+ scanSchemaDir(dir, schemaName, callback) {
19
+ const files = fs.readdirSync(dir);
20
+ files.forEach((file) => {
21
+ const filePath = path.join(dir, file);
22
+ const stat = fs.statSync(filePath);
23
+ if (stat.isDirectory()) {
24
+ this.scanSchemaDir(filePath, schemaName, callback);
25
+ }
26
+ else if (file.endsWith(".ts")) {
27
+ this.processSchemaFile(filePath, schemaName, callback);
28
+ }
29
+ });
30
+ }
31
+ processSchemaFile(filePath, schemaName, callback) {
32
+ const content = fs.readFileSync(filePath, "utf-8");
33
+ const ast = parse(content, {
34
+ sourceType: "module",
35
+ plugins: ["typescript"],
36
+ });
37
+ traverse.default(ast, {
38
+ VariableDeclarator: (path) => {
39
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
40
+ callback(path.node.init || path.node);
41
+ }
42
+ },
43
+ TSTypeAliasDeclaration: (path) => {
44
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
45
+ callback(path.node.typeAnnotation);
46
+ }
47
+ },
48
+ TSInterfaceDeclaration: (path) => {
49
+ if (t.isIdentifier(path.node.id, { name: schemaName })) {
50
+ callback(path.node);
51
+ }
52
+ },
53
+ });
54
+ }
55
+ extractTypesFromSchema(schema, dataType) {
56
+ const result = dataType === "params" ? [] : {};
57
+ const handleProperty = (property) => {
58
+ const key = property.key.name;
59
+ const typeAnnotation = property.typeAnnotation?.typeAnnotation?.type;
60
+ const type = this.getTypeFromAnnotation(typeAnnotation);
61
+ const isOptional = !!property.optional; // check if property is optional
62
+ let description = "";
63
+ // get comments for field
64
+ if (property.trailingComments && property.trailingComments.length) {
65
+ description = property.trailingComments[0].value.trim(); // get first comment
66
+ }
67
+ const field = {
68
+ type: type,
69
+ description: description,
70
+ };
71
+ if (dataType === "params") {
72
+ // @ts-ignore
73
+ result.push({
74
+ name: key,
75
+ in: "query",
76
+ schema: field,
77
+ required: !isOptional,
78
+ });
79
+ }
80
+ else {
81
+ result[key] = field;
82
+ }
83
+ };
84
+ if (schema.body?.body) {
85
+ schema.body.body.forEach(handleProperty);
86
+ }
87
+ if (schema.type === "TSTypeLiteral" && schema.members) {
88
+ schema.members.forEach(handleProperty);
89
+ }
90
+ return result;
91
+ }
92
+ getTypeFromAnnotation(type) {
93
+ switch (type) {
94
+ case "TSStringKeyword":
95
+ return "string";
96
+ case "TSNumberKeyword":
97
+ return "number";
98
+ case "TSBooleanKeyword":
99
+ return "boolean";
100
+ // Add other cases as needed.
101
+ default:
102
+ return "object"; // fallback to object for unknown types
103
+ }
104
+ }
105
+ createRequestBodySchema(body) {
106
+ return {
107
+ content: {
108
+ "application/json": {
109
+ schema: {
110
+ type: "object",
111
+ properties: body,
112
+ },
113
+ },
114
+ },
115
+ };
116
+ }
117
+ createResponseSchema(responses) {
118
+ return {
119
+ 200: {
120
+ description: "Successful response",
121
+ content: {
122
+ "application/json": {
123
+ schema: {
124
+ type: "object",
125
+ properties: responses,
126
+ },
127
+ },
128
+ },
129
+ },
130
+ };
131
+ }
132
+ getSchemaContent({ paramsType, bodyType, responseType }) {
133
+ const paramsSchema = paramsType
134
+ ? this.findSchemaDefinition(paramsType)
135
+ : null;
136
+ const bodySchema = bodyType ? this.findSchemaDefinition(bodyType) : null;
137
+ const responseSchema = responseType
138
+ ? this.findSchemaDefinition(responseType)
139
+ : null;
140
+ let params = paramsSchema
141
+ ? this.extractTypesFromSchema(paramsSchema, "params")
142
+ : [];
143
+ let body = bodySchema
144
+ ? this.extractTypesFromSchema(bodySchema, "body")
145
+ : {};
146
+ let responses = responseSchema
147
+ ? this.extractTypesFromSchema(responseSchema, "responses")
148
+ : {};
149
+ return {
150
+ params,
151
+ body,
152
+ responses,
153
+ };
154
+ }
155
+ }
@@ -0,0 +1,66 @@
1
+ export function capitalize(string) {
2
+ return string.charAt(0).toUpperCase() + string.slice(1);
3
+ }
4
+ export function extractJSDocComments(path) {
5
+ const comments = path.node.leadingComments;
6
+ let summary = "";
7
+ let description = "";
8
+ let paramsType = "";
9
+ let bodyType = "";
10
+ let responseType = "";
11
+ let isOpenApi = false;
12
+ if (comments) {
13
+ comments.forEach((comment) => {
14
+ const commentValue = cleanComment(comment.value);
15
+ isOpenApi = commentValue.includes("@openapi");
16
+ if (!summary) {
17
+ const summaryIndex = isOpenApi ? 1 : 0;
18
+ summary = commentValue.split("\n")[summaryIndex];
19
+ }
20
+ if (commentValue.includes("@desc")) {
21
+ const regex = /@desc:\s*(.*)/;
22
+ description = commentValue.match(regex)[1].trim();
23
+ }
24
+ if (commentValue.includes("@params")) {
25
+ paramsType = extractTypeFromComment(commentValue, "@params");
26
+ }
27
+ if (commentValue.includes("@body")) {
28
+ bodyType = extractTypeFromComment(commentValue, "@body");
29
+ }
30
+ if (commentValue.includes("@response")) {
31
+ responseType = extractTypeFromComment(commentValue, "@response");
32
+ }
33
+ });
34
+ }
35
+ return {
36
+ summary,
37
+ description,
38
+ paramsType,
39
+ bodyType,
40
+ responseType,
41
+ isOpenApi,
42
+ };
43
+ }
44
+ export function extractTypeFromComment(commentValue, tag) {
45
+ return commentValue.match(new RegExp(`${tag}\\s*:\\s*(\\w+)`))?.[1] || "";
46
+ }
47
+ export function cleanComment(commentValue) {
48
+ return commentValue.replace(/\*\s*/g, "").trim();
49
+ }
50
+ export function cleanSpec(spec) {
51
+ const propsToRemove = [
52
+ "apiDir",
53
+ "schemaDir",
54
+ "docsUrl",
55
+ "ui",
56
+ "outputFile",
57
+ "includeOpenApiRoutes",
58
+ ];
59
+ const newSpec = { ...spec };
60
+ propsToRemove.forEach((key) => delete newSpec[key]);
61
+ return newSpec;
62
+ }
63
+ export function getOperationId(routePath, method) {
64
+ const operation = routePath.replaceAll(/\//g, "-").replace(/^-/, "");
65
+ return `${method}-${operation}`;
66
+ }
@@ -0,0 +1,20 @@
1
+ export default {
2
+ openapi: "3.0.0",
3
+ info: {
4
+ title: "API Documentation",
5
+ version: "1.0.0",
6
+ description: "This is the OpenAPI specification for your project.",
7
+ },
8
+ servers: [
9
+ {
10
+ url: "http://localhost:3000",
11
+ description: "Local development server",
12
+ },
13
+ ],
14
+ paths: {},
15
+ apiPath: "./src/app/api",
16
+ docsUrl: "api-docs",
17
+ ui: "swagger",
18
+ outputPath: "./public/swagger.json",
19
+ includeOpenApiRoutes: true,
20
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.0.3",
3
+ "version": "0.0.6",
4
4
  "description": "Automatically generate OpenAPI documentation for Next.js API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
8
8
  "typings": "dist/index.d.ts",
9
9
  "bin": {
10
- "next-openapi-gen": "./dist/index"
10
+ "next-openapi-gen": "./dist/index.js"
11
11
  },
12
+ "files": [
13
+ "dist"
14
+ ],
12
15
  "scripts": {
13
16
  "build": "tsc",
14
17
  "prepare": "npm run build"
@@ -1,168 +0,0 @@
1
- import { parse } from "@babel/parser";
2
- import traverse from "@babel/traverse";
3
- import * as t from "@babel/types";
4
- import fs from "fs";
5
- import path from "path";
6
- import ora from "ora";
7
- const apiDir = path.resolve("./src/app/api");
8
- const schemaDir = path.resolve("./src/types/schemas");
9
- const swaggerPaths = {};
10
- function extractSchemaFromZod(schemaNode) {
11
- const properties = {};
12
- schemaNode.arguments.forEach((arg) => {
13
- if (t.isObjectExpression(arg)) {
14
- arg.properties.forEach((prop) => {
15
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
16
- // @ts-ignore
17
- const type = prop.value.callee.property.name;
18
- properties[prop.key.name] = { type };
19
- }
20
- });
21
- }
22
- });
23
- return { type: "object", properties };
24
- }
25
- function findSchemaDefinition(schemaName) {
26
- let schemaNode = null;
27
- function processSchemaFile(filePath) {
28
- const content = fs.readFileSync(filePath, "utf-8");
29
- const ast = parse(content, {
30
- sourceType: "module",
31
- plugins: ["typescript"],
32
- });
33
- traverse.default(ast, {
34
- VariableDeclarator(path) {
35
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
36
- if (t.isCallExpression(path.node.init)) {
37
- schemaNode = path.node.init;
38
- }
39
- }
40
- },
41
- });
42
- }
43
- function scanSchemaDir(dir) {
44
- const files = fs.readdirSync(dir);
45
- files.forEach((file) => {
46
- const filePath = path.join(dir, file);
47
- const stat = fs.statSync(filePath);
48
- if (stat.isDirectory()) {
49
- scanSchemaDir(filePath);
50
- }
51
- else if (file.endsWith(".ts")) {
52
- processSchemaFile(filePath);
53
- }
54
- });
55
- }
56
- scanSchemaDir(schemaDir);
57
- return schemaNode;
58
- }
59
- function processFile(filePath) {
60
- const content = fs.readFileSync(filePath, "utf-8");
61
- const ast = parse(content, { sourceType: "module", plugins: ["typescript"] });
62
- traverse.default(ast, {
63
- ExportNamedDeclaration(path) {
64
- const declaration = path.node.declaration;
65
- if (t.isVariableDeclaration(declaration)) {
66
- declaration.declarations.forEach((decl) => {
67
- if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
68
- const varName = decl.id.name;
69
- let schema = {};
70
- let options = {};
71
- if (varName === "POST" ||
72
- varName === "GET" ||
73
- varName === "PUT" ||
74
- varName === "PATCH" ||
75
- varName === "DELETE") {
76
- const handler = decl.init;
77
- // Handle schema definition
78
- if (fs.existsSync(schemaDir) &&
79
- t.isCallExpression(handler) &&
80
- t.isIdentifier(handler.callee, { name: "withAPI" })) {
81
- const [schemaIdentifier] = handler.arguments;
82
- if (t.isIdentifier(schemaIdentifier)) {
83
- const schemaNode = findSchemaDefinition(schemaIdentifier.name);
84
- const optionsNode = schemaNode.arguments[0];
85
- if (schemaNode) {
86
- // @TODO: add z.array tracking
87
- if (t.isObjectExpression(optionsNode)) {
88
- schema = extractSchemaFromZod(schemaNode);
89
- options = optionsNode.properties.reduce((acc, prop) => {
90
- if (t.isObjectProperty(prop) &&
91
- t.isIdentifier(prop.key)) {
92
- // @ts-ignore
93
- acc[prop.key.name] = prop.value.callee.property.name;
94
- }
95
- return acc;
96
- }, {});
97
- }
98
- }
99
- }
100
- }
101
- const method = varName.toLowerCase();
102
- const routePath = filePath
103
- .replace(apiDir, "")
104
- .replace("route.ts", "")
105
- .replaceAll("\\", "/")
106
- .replace(/\/$/, "");
107
- const rootPath = routePath.split("/")[1];
108
- if (!swaggerPaths[routePath]) {
109
- swaggerPaths[routePath] = {};
110
- }
111
- swaggerPaths[routePath][method] = {
112
- operationId: options.opId,
113
- description: options.desc,
114
- tags: [rootPath],
115
- requestBody: {
116
- content: {
117
- "application/json": {
118
- schema,
119
- },
120
- },
121
- },
122
- responses: {
123
- 200: {
124
- description: "Successful response",
125
- content: {
126
- "application/json": {
127
- schema: options.res,
128
- },
129
- },
130
- },
131
- 400: {
132
- description: "Validation error",
133
- },
134
- 500: {
135
- description: "Server error",
136
- },
137
- },
138
- };
139
- }
140
- }
141
- });
142
- }
143
- },
144
- });
145
- }
146
- function scanDir(dir) {
147
- const files = fs.readdirSync(dir);
148
- files.forEach((file) => {
149
- const filePath = path.join(dir, file);
150
- const stat = fs.statSync(filePath);
151
- if (stat.isDirectory()) {
152
- scanDir(filePath);
153
- }
154
- else if (file.endsWith(".ts")) {
155
- processFile(filePath);
156
- }
157
- });
158
- }
159
- export async function generateOpenapiSpec() {
160
- const spinner = ora("Generating openapi specification...\n").start();
161
- scanDir(apiDir);
162
- const openapiPath = path.resolve("./next.openapi.json");
163
- const openapiSpec = JSON.parse(fs.readFileSync(openapiPath, "utf-8"));
164
- openapiSpec.paths = swaggerPaths;
165
- const outputPath = path.resolve(openapiSpec.outputPath);
166
- fs.writeFileSync(outputPath, JSON.stringify(openapiSpec, null, 2));
167
- spinner.succeed(`Swagger spec generated at ${outputPath}`);
168
- }