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.
- package/dist/app/api/register/route.js +20 -0
- package/dist/commands/generate.js +18 -0
- package/dist/commands/init.js +6 -22
- package/dist/index.js +8 -4
- package/dist/lib/main.js +23 -0
- package/dist/lib/openapi-generator.js +34 -0
- package/dist/lib/route-processor.js +135 -0
- package/dist/lib/schema-processor.js +155 -0
- package/dist/lib/utils.js +66 -0
- package/dist/openapi-template.js +20 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/dist/cli/init.js +0 -0
- package/dist/commands/generate-openapi-spec.js +0 -168
- package/dist/generate-openapi-spec.js +0 -0
- package/dist/init.js +0 -30
- package/dist/lib/generate-openapi-spec.js +0 -95
- package/src/commands/generate-openapi-spec.ts +0 -201
- package/src/commands/init.ts +0 -110
- package/src/index.ts +0 -27
- package/todo.md +0 -12
- package/tsconfig.json +0 -17
- /package/dist/{cli/generate-openapi-spec.js → types/index.js} +0 -0
|
@@ -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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
83
|
-
|
|
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 {
|
|
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
|
|
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(
|
|
21
|
+
.action(generate);
|
|
18
22
|
program.parse(process.argv);
|
package/dist/lib/main.js
ADDED
|
@@ -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
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
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -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
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
|
-
}
|
|
File without changes
|