next-openapi-gen 0.0.2 → 0.0.5

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,20 @@
1
+ /**
2
+ * Register a new user !!!
3
+ * @desc: Create a new link for authenticated user
4
+ * @params: QueryParams
5
+ * @body: Input
6
+ * @response: Response
7
+ */
8
+ export async function POST(req) {
9
+ const data = await req.json();
10
+ return NextResponse.json({}, { status: 201 });
11
+ }
12
+ /**
13
+ * Get user
14
+ * @desc: Create a new link
15
+ * @params: QueryParams
16
+ * @response: Input
17
+ */
18
+ export async function GET(req) {
19
+ return NextResponse.json({}, { status: 201 });
20
+ }
@@ -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,23 @@
1
+ import { OpenApiGenerator } from "./openapi-generator";
2
+ const openApiTemplate = {
3
+ openapi: "3.0.0",
4
+ info: {
5
+ title: "My API",
6
+ version: "1.0.0",
7
+ description: "API description",
8
+ },
9
+ servers: [
10
+ {
11
+ url: "http://localhost:3000",
12
+ description: "Development server",
13
+ },
14
+ ],
15
+ paths: {},
16
+ apiPath: "src/api",
17
+ docsUrl: "http://localhost:3000/docs",
18
+ ui: "http://localhost:3000/ui",
19
+ outputPath: "output/openapi.json",
20
+ };
21
+ const generator = new OpenApiGenerator();
22
+ const apiDocumentation = generator.generate();
23
+ console.log(JSON.stringify(apiDocumentation, null, 2));
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
4
4
  "description": "Automatically generate OpenAPI documentation for Next.js API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/dist/cli/init.js DELETED
File without changes
@@ -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
- }
File without changes
package/dist/init.js DELETED
@@ -1,30 +0,0 @@
1
- import path from "path";
2
- import fse from "fs-extra";
3
- import ora from "ora";
4
- // Definicja szablonu dla pliku OpenAPI
5
- const openApiTemplate = {
6
- openapi: "3.0.0",
7
- info: {
8
- title: "API Documentation",
9
- version: "1.0.0",
10
- description: "This is the OpenAPI specification for your project.",
11
- },
12
- servers: [
13
- {
14
- url: "http://localhost:3000",
15
- description: "Local development server",
16
- },
17
- ],
18
- paths: {},
19
- };
20
- export const init = async () => {
21
- const spinner = ora("Initializing project with OpenAPI template...\n").start();
22
- try {
23
- const outputPath = path.join(process.cwd(), "next.openapi.json");
24
- await fse.writeJson(outputPath, openApiTemplate, { spaces: 2 });
25
- spinner.succeed(`OpenAPI template created successfully at ${outputPath}`);
26
- }
27
- catch (error) {
28
- spinner.fail(`Failed to initialize project: ${error.message}`);
29
- }
30
- };
@@ -1,95 +0,0 @@
1
- // import path from "path";
2
- // import fs from "fs-extra";
3
- // // Definicja typu dla konfiguracji OpenAPI
4
- // interface OpenApiConfig {
5
- // openapi: string;
6
- // info: {
7
- // title: string;
8
- // description: string;
9
- // version: string;
10
- // };
11
- // servers: { url: string; description: string }[];
12
- // paths: Record<string, any>;
13
- // components: {
14
- // schemas: Record<string, any>;
15
- // };
16
- // }
17
- // // Domyślne opcje OpenAPI
18
- // const defaultOpenApiConfig: OpenApiConfig = {
19
- // openapi: "3.0.0",
20
- // info: {
21
- // title: "Next.js API",
22
- // description: "This is the API documentation for the Next.js application.",
23
- // version: "1.0.0",
24
- // },
25
- // servers: [
26
- // {
27
- // url: "http://localhost:3000",
28
- // description: "Local server",
29
- // },
30
- // ],
31
- // paths: {},
32
- // components: {
33
- // schemas: {},
34
- // },
35
- // };
36
- // export function generateOpenApiSpec(): void {
37
- // const projectRoot = process.cwd();
38
- // // Ścieżki do API, schematów i modeli
39
- // const apiDir = path.join(projectRoot, "pages/api");
40
- // const schemaDir = path.join(projectRoot, "schemas");
41
- // const modelsDir = path.join(projectRoot, "models");
42
- // // Generowanie pliku next.openapi.json
43
- // const openApiPath = path.join(projectRoot, "next.openapi.json");
44
- // // Generowanie paths i schemas na podstawie plików z katalogów
45
- // const openApiSpec: OpenApiConfig = {
46
- // ...defaultOpenApiConfig,
47
- // paths: generatePathsFromApiDir(apiDir),
48
- // components: {
49
- // schemas: generateSchemasFromDir(schemaDir),
50
- // },
51
- // };
52
- // // Zapisanie pliku next.openapi.json
53
- // fs.writeJsonSync(openApiPath, openApiSpec, { spaces: 2 });
54
- // console.log(`OpenAPI spec generated at: ${openApiPath}`);
55
- // }
56
- // // Funkcja generująca ścieżki na podstawie katalogu API
57
- // function generatePathsFromApiDir(apiDir: string): Record<string, any> {
58
- // const paths: Record<string, any> = {};
59
- // if (fs.existsSync(apiDir)) {
60
- // const files = fs.readdirSync(apiDir);
61
- // files.forEach((file) => {
62
- // const filePath = path.join(apiDir, file);
63
- // if (fs.statSync(filePath).isFile() && file.endsWith(".ts")) {
64
- // const route = `/api/${file.replace(".ts", "")}`;
65
- // paths[route] = {
66
- // get: {
67
- // summary: `Get ${route}`,
68
- // responses: {
69
- // 200: {
70
- // description: "Successful response",
71
- // },
72
- // },
73
- // },
74
- // };
75
- // }
76
- // });
77
- // }
78
- // return paths;
79
- // }
80
- // // Funkcja generująca schematy na podstawie katalogu schematów
81
- // function generateSchemasFromDir(schemaDir: string): Record<string, any> {
82
- // const schemas: Record<string, any> = {};
83
- // if (fs.existsSync(schemaDir)) {
84
- // const files = fs.readdirSync(schemaDir);
85
- // files.forEach((file) => {
86
- // const schemaPath = path.join(schemaDir, file);
87
- // if (fs.statSync(schemaPath).isFile() && file.endsWith(".json")) {
88
- // const schema = fs.readJsonSync(schemaPath);
89
- // const schemaName = file.replace(".json", "");
90
- // schemas[schemaName] = schema;
91
- // }
92
- // });
93
- // }
94
- // return schemas;
95
- // }
@@ -1,201 +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
-
8
- const apiDir = path.resolve("./src/app/api");
9
- const schemaDir = path.resolve("./src/types/schemas");
10
- const swaggerPaths = {};
11
-
12
- function extractSchemaFromZod(schemaNode) {
13
- const properties = {};
14
- schemaNode.arguments.forEach((arg) => {
15
- if (t.isObjectExpression(arg)) {
16
- arg.properties.forEach((prop) => {
17
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
18
- // @ts-ignore
19
- const type = prop.value.callee.property.name;
20
- properties[prop.key.name] = { type };
21
- }
22
- });
23
- }
24
- });
25
- return { type: "object", properties };
26
- }
27
-
28
- function findSchemaDefinition(schemaName) {
29
- let schemaNode = null;
30
-
31
- function processSchemaFile(filePath) {
32
- const content = fs.readFileSync(filePath, "utf-8");
33
- const ast = parse(content, {
34
- sourceType: "module",
35
- plugins: ["typescript"],
36
- });
37
-
38
- traverse.default(ast, {
39
- VariableDeclarator(path) {
40
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
41
- if (t.isCallExpression(path.node.init)) {
42
- schemaNode = path.node.init;
43
- }
44
- }
45
- },
46
- });
47
- }
48
-
49
- function scanSchemaDir(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
- scanSchemaDir(filePath);
56
- } else if (file.endsWith(".ts")) {
57
- processSchemaFile(filePath);
58
- }
59
- });
60
- }
61
-
62
- scanSchemaDir(schemaDir);
63
- return schemaNode;
64
- }
65
-
66
- function processFile(filePath) {
67
- const content = fs.readFileSync(filePath, "utf-8");
68
- const ast = parse(content, { sourceType: "module", plugins: ["typescript"] });
69
-
70
- traverse.default(ast, {
71
- ExportNamedDeclaration(path) {
72
- const declaration = path.node.declaration;
73
-
74
- if (t.isVariableDeclaration(declaration)) {
75
- declaration.declarations.forEach((decl) => {
76
- if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
77
- const varName = decl.id.name;
78
- let schema = {};
79
- let options: any = {};
80
-
81
- if (
82
- varName === "POST" ||
83
- varName === "GET" ||
84
- varName === "PUT" ||
85
- varName === "PATCH" ||
86
- varName === "DELETE"
87
- ) {
88
- const handler = decl.init;
89
-
90
- // Handle schema definition
91
- if (
92
- fs.existsSync(schemaDir) &&
93
- t.isCallExpression(handler) &&
94
- t.isIdentifier(handler.callee, { name: "withAPI" })
95
- ) {
96
- const [schemaIdentifier] = handler.arguments;
97
-
98
- if (t.isIdentifier(schemaIdentifier)) {
99
- const schemaNode = findSchemaDefinition(
100
- schemaIdentifier.name
101
- );
102
-
103
- const optionsNode = schemaNode.arguments[0];
104
-
105
- if (schemaNode) {
106
- // @TODO: add z.array tracking
107
- if (t.isObjectExpression(optionsNode)) {
108
- schema = extractSchemaFromZod(schemaNode);
109
-
110
- options = optionsNode.properties.reduce((acc, prop) => {
111
- if (
112
- t.isObjectProperty(prop) &&
113
- t.isIdentifier(prop.key)
114
- ) {
115
- // @ts-ignore
116
- acc[prop.key.name] = prop.value.callee.property.name;
117
- }
118
- return acc;
119
- }, {});
120
- }
121
- }
122
- }
123
- }
124
-
125
- const method = varName.toLowerCase();
126
- const routePath = filePath
127
- .replace(apiDir, "")
128
- .replace("route.ts", "")
129
- .replaceAll("\\", "/")
130
- .replace(/\/$/, "");
131
-
132
- const rootPath = routePath.split("/")[1];
133
-
134
- if (!swaggerPaths[routePath]) {
135
- swaggerPaths[routePath] = {};
136
- }
137
-
138
- swaggerPaths[routePath][method] = {
139
- operationId: options.opId,
140
- description: options.desc,
141
- tags: [rootPath],
142
- requestBody: {
143
- content: {
144
- "application/json": {
145
- schema,
146
- },
147
- },
148
- },
149
- responses: {
150
- 200: {
151
- description: "Successful response",
152
- content: {
153
- "application/json": {
154
- schema: options.res,
155
- },
156
- },
157
- },
158
- 400: {
159
- description: "Validation error",
160
- },
161
- 500: {
162
- description: "Server error",
163
- },
164
- },
165
- };
166
- }
167
- }
168
- });
169
- }
170
- },
171
- });
172
- }
173
-
174
- function scanDir(dir) {
175
- const files = fs.readdirSync(dir);
176
- files.forEach((file) => {
177
- const filePath = path.join(dir, file);
178
- const stat = fs.statSync(filePath);
179
- if (stat.isDirectory()) {
180
- scanDir(filePath);
181
- } else if (file.endsWith(".ts")) {
182
- processFile(filePath);
183
- }
184
- });
185
- }
186
-
187
- export async function generateOpenapiSpec() {
188
- const spinner = ora("Generating openapi specification...\n").start();
189
-
190
- scanDir(apiDir);
191
-
192
- const openapiPath = path.resolve("./next.openapi.json");
193
- const openapiSpec = JSON.parse(fs.readFileSync(openapiPath, "utf-8"));
194
-
195
- openapiSpec.paths = swaggerPaths;
196
-
197
- const outputPath = path.resolve(openapiSpec.outputPath);
198
- fs.writeFileSync(outputPath, JSON.stringify(openapiSpec, null, 2));
199
-
200
- spinner.succeed(`Swagger spec generated at ${outputPath}`);
201
- }
@@ -1,110 +0,0 @@
1
- import path from "path";
2
- import fse from "fs-extra";
3
- import fs from "fs";
4
- import ora from "ora";
5
- import { exec } from "child_process";
6
- import util from "util";
7
-
8
- const execPromise = util.promisify(exec);
9
-
10
- const spinner = ora("Initializing project with OpenAPI template...\n");
11
-
12
- const openApiTemplate = {
13
- openapi: "3.0.0",
14
- info: {
15
- title: "API Documentation",
16
- version: "1.0.0",
17
- description: "This is the OpenAPI specification for your project.",
18
- },
19
- servers: [
20
- {
21
- url: "http://localhost:3000",
22
- description: "Local development server",
23
- },
24
- ],
25
- paths: {},
26
- apiPath: "./src/app/api",
27
- docsUrl: "api-docs",
28
- ui: "swagger",
29
- outputPath: "./public/swagger.json",
30
- };
31
-
32
- const getPackageManager = async () => {
33
- if (fs.existsSync(path.join(process.cwd(), "yarn.lock"))) {
34
- return "yarn";
35
- }
36
- if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml"))) {
37
- return "pnpm";
38
- }
39
- return "npm";
40
- };
41
-
42
- async function createDocsPage() {
43
- const paths = ["app", "api-docs"];
44
- const srcPath = path.join(process.cwd(), "src");
45
-
46
- if (fs.existsSync(srcPath)) {
47
- paths.unshift("src");
48
- }
49
-
50
- const docsDir = path.join(process.cwd(), ...paths);
51
- await fs.promises.mkdir(docsDir, { recursive: true });
52
-
53
- const swaggerComponent = `
54
- import "swagger-ui-react/swagger-ui.css";
55
-
56
- import dynamic from "next/dynamic";
57
-
58
- const SwaggerUI = dynamic(() => import("swagger-ui-react"), {
59
- ssr: false,
60
- loading: () => <p>Loading Component...</p>,
61
- });
62
-
63
- export default async function ApiDocsPage() {
64
- return (
65
- <section>
66
- <SwaggerUI url="/swagger.json" />
67
- </section>
68
- );
69
- }
70
- `;
71
-
72
- const componentPath = path.join(docsDir, "page.tsx");
73
- await fs.promises.writeFile(componentPath, swaggerComponent.trim());
74
- spinner.succeed(`Created ${paths.join("/")}/page.tsx for Swagger UI.`);
75
- }
76
-
77
- async function installSwagger() {
78
- const packageManager = await getPackageManager();
79
- const installCmd = `${packageManager} ${
80
- packageManager === "npm" ? "install" : "add"
81
- }`;
82
-
83
- spinner.succeed("Installing swagger-ui-react...");
84
- const resp = await execPromise(`${installCmd} swagger-ui-react`);
85
- spinner.succeed("Successfully installed swagger-ui-react.");
86
- }
87
-
88
- function extendOpenApiTemplate(spec, options) {
89
- spec.ui = options.ui ?? spec.ui;
90
- spec.docsUrl = options.docsUrl ?? spec.docsUrl;
91
- }
92
-
93
- export async function init(ui: string, docsUrl: string) {
94
- spinner.start();
95
-
96
- try {
97
- const outputPath = path.join(process.cwd(), "next.openapi.json");
98
- extendOpenApiTemplate(openApiTemplate, { docsUrl, ui });
99
-
100
- await fse.writeJson(outputPath, openApiTemplate, { spaces: 2 });
101
- spinner.succeed(`Created OpenAPI template in next.openapi.json`);
102
-
103
- if (ui === "swagger") {
104
- createDocsPage();
105
- installSwagger();
106
- }
107
- } catch (error) {
108
- spinner.fail(`Failed to initialize project: ${error.message}`);
109
- }
110
- }
package/src/index.ts DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from "commander";
4
-
5
- import { init } from "./commands/init.js";
6
- import { generateOpenapiSpec } from "./commands/generate-openapi-spec.js";
7
-
8
- const program = new Command();
9
-
10
- program
11
- .name("next-openapi-gen")
12
- .version("0.0.1")
13
- .description(
14
- "Super fast and easy way to generate OpenAPI documentation for Next.js"
15
- );
16
-
17
- program
18
- .command("init <ui> <docs-url>")
19
- .description("Initialize a openapi specification")
20
- .action(init);
21
-
22
- program
23
- .command("generate")
24
- .description("Generate a specification based on api routes")
25
- .action(generateOpenapiSpec);
26
-
27
- program.parse(process.argv);
package/todo.md DELETED
@@ -1,12 +0,0 @@
1
- next-openapi-gen
2
- next-apidoc-gen
3
-
4
-
5
- OpenAPI generator for Next.js
6
-
7
- npx create-email init my-project
8
- npx create-email generate component
9
-
10
-
11
- node ./dist/index.js init swagger api-docs
12
- node ./dist/index.js generate
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "esnext",
4
- "lib": ["es2022"],
5
- // "target": "ESNext",
6
- "module": "esnext",
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "esModuleInterop": true,
10
- "moduleResolution": "node",
11
- "strict": false,
12
- "types": ["node"],
13
- "skipLibCheck": true
14
- },
15
- "include": ["src"],
16
- "exclude": ["dist", "build", "node_modules"]
17
- }