next-openapi-gen 0.10.4 → 1.0.0
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/cli.d.ts +4 -0
- package/dist/cli.js +8599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8645 -26
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +7965 -0
- package/dist/react-router/index.d.ts +1 -0
- package/dist/react-router/index.js +7134 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +7134 -0
- package/package.json +102 -79
- package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
- package/templates/init/ui/nextjs/redoc.tsx +11 -0
- package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
- package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
- package/templates/init/ui/nextjs/swagger.tsx +17 -0
- package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
- package/templates/init/ui/reactrouter/redoc.tsx +9 -0
- package/templates/init/ui/reactrouter/scalar.tsx +14 -0
- package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
- package/templates/init/ui/reactrouter/swagger.tsx +11 -0
- package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
- package/templates/init/ui/tanstack/redoc.tsx +14 -0
- package/templates/init/ui/tanstack/scalar.tsx +19 -0
- package/templates/init/ui/tanstack/stoplight.tsx +15 -0
- package/templates/init/ui/tanstack/swagger.tsx +16 -0
- package/templates/init/ui/template-types.d.ts +9 -0
- package/README.md +0 -1047
- package/dist/commands/generate.js +0 -24
- package/dist/commands/init.js +0 -194
- package/dist/components/redoc.js +0 -17
- package/dist/components/swagger.js +0 -21
- package/dist/lib/app-router-strategy.js +0 -66
- package/dist/lib/drizzle-zod-processor.js +0 -329
- package/dist/lib/logger.js +0 -39
- package/dist/lib/openapi-generator.js +0 -171
- package/dist/lib/pages-router-strategy.js +0 -198
- package/dist/lib/route-processor.js +0 -347
- package/dist/lib/router-strategy.js +0 -1
- package/dist/lib/schema-processor.js +0 -1612
- package/dist/lib/utils.js +0 -284
- package/dist/lib/zod-converter.js +0 -2133
- package/dist/openapi-template.js +0 -99
- package/dist/types.js +0 -1
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { RouteProcessor } from "./route-processor.js";
|
|
4
|
-
import { cleanSpec } from "./utils.js";
|
|
5
|
-
import { logger } from "./logger.js";
|
|
6
|
-
export class OpenApiGenerator {
|
|
7
|
-
config;
|
|
8
|
-
template;
|
|
9
|
-
routeProcessor;
|
|
10
|
-
constructor(opts = {}) {
|
|
11
|
-
const templatePath = opts.templatePath || path.resolve("./next.openapi.json");
|
|
12
|
-
this.template = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
|
|
13
|
-
this.config = this.getConfig();
|
|
14
|
-
this.routeProcessor = new RouteProcessor(this.config);
|
|
15
|
-
// Initialize logger
|
|
16
|
-
logger.init(this.config);
|
|
17
|
-
}
|
|
18
|
-
getConfig() {
|
|
19
|
-
// @ts-ignore
|
|
20
|
-
const { apiDir, routerType, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, ignoreRoutes, schemaType = "typescript", schemaFiles, defaultResponseSet, responseSets, errorConfig, debug } = this.template;
|
|
21
|
-
return {
|
|
22
|
-
apiDir: apiDir || "./src/app/api",
|
|
23
|
-
routerType: routerType || "app",
|
|
24
|
-
schemaDir: schemaDir || "./src",
|
|
25
|
-
docsUrl: docsUrl || "api-docs",
|
|
26
|
-
ui: ui || "scalar",
|
|
27
|
-
outputFile: outputFile || "openapi.json",
|
|
28
|
-
outputDir: outputDir || "./public",
|
|
29
|
-
includeOpenApiRoutes: includeOpenApiRoutes || false,
|
|
30
|
-
ignoreRoutes: ignoreRoutes || [],
|
|
31
|
-
schemaType,
|
|
32
|
-
schemaFiles: schemaFiles || [],
|
|
33
|
-
defaultResponseSet,
|
|
34
|
-
responseSets,
|
|
35
|
-
errorConfig,
|
|
36
|
-
debug: debug || false,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
generate() {
|
|
40
|
-
logger.log("Starting OpenAPI generation...");
|
|
41
|
-
const { apiDir } = this.config;
|
|
42
|
-
// Check if app router structure exists
|
|
43
|
-
let appRouterApiDir = "";
|
|
44
|
-
if (fs.existsSync(path.join(path.dirname(apiDir), "app", "api"))) {
|
|
45
|
-
appRouterApiDir = path.join(path.dirname(apiDir), "app", "api");
|
|
46
|
-
logger.debug(`Found app router API directory at ${appRouterApiDir}`);
|
|
47
|
-
}
|
|
48
|
-
// Scan pages router routes
|
|
49
|
-
this.routeProcessor.scanApiRoutes(apiDir);
|
|
50
|
-
// If app router directory exists, scan it as well
|
|
51
|
-
if (appRouterApiDir) {
|
|
52
|
-
this.routeProcessor.scanApiRoutes(appRouterApiDir);
|
|
53
|
-
}
|
|
54
|
-
this.template.paths = this.routeProcessor.getSwaggerPaths();
|
|
55
|
-
// Add server URL for examples if not already defined
|
|
56
|
-
if (!this.template.servers || this.template.servers.length === 0) {
|
|
57
|
-
this.template.servers = [
|
|
58
|
-
{
|
|
59
|
-
url: this.template.basePath || "",
|
|
60
|
-
description: "API server",
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
|
-
// Ensure there's a components section if not already defined
|
|
65
|
-
if (!this.template.components) {
|
|
66
|
-
this.template.components = {};
|
|
67
|
-
}
|
|
68
|
-
// Add schemas section if not already defined
|
|
69
|
-
if (!this.template.components.schemas) {
|
|
70
|
-
this.template.components.schemas = {};
|
|
71
|
-
}
|
|
72
|
-
// Generate error responses using errorConfig or manual definitions
|
|
73
|
-
if (!this.template.components.responses) {
|
|
74
|
-
this.template.components.responses = {};
|
|
75
|
-
}
|
|
76
|
-
const errorConfig = this.config.errorConfig;
|
|
77
|
-
if (errorConfig) {
|
|
78
|
-
this.generateErrorResponsesFromConfig(errorConfig);
|
|
79
|
-
}
|
|
80
|
-
else if (this.config.errorDefinitions) {
|
|
81
|
-
// Use manual definitions (existing logic - if exists)
|
|
82
|
-
Object.entries(this.config.errorDefinitions).forEach(([code, errorDef]) => {
|
|
83
|
-
this.template.components.responses[code] =
|
|
84
|
-
this.createErrorResponseComponent(code, errorDef);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
// Get defined schemas from the processor
|
|
88
|
-
const definedSchemas = this.routeProcessor
|
|
89
|
-
.getSchemaProcessor()
|
|
90
|
-
.getDefinedSchemas();
|
|
91
|
-
if (definedSchemas && Object.keys(definedSchemas).length > 0) {
|
|
92
|
-
this.template.components.schemas = {
|
|
93
|
-
...this.template.components.schemas,
|
|
94
|
-
...definedSchemas,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
const openapiSpec = cleanSpec(this.template);
|
|
98
|
-
logger.log("OpenAPI generation completed");
|
|
99
|
-
return openapiSpec;
|
|
100
|
-
}
|
|
101
|
-
generateErrorResponsesFromConfig(errorConfig) {
|
|
102
|
-
const { template, codes, variables: globalVars = {} } = errorConfig;
|
|
103
|
-
Object.entries(codes).forEach(([errorCode, config]) => {
|
|
104
|
-
const httpStatus = (config.httpStatus || this.guessHttpStatus(errorCode)).toString();
|
|
105
|
-
// Merge variables: global + per-code + built-in
|
|
106
|
-
const allVariables = {
|
|
107
|
-
...globalVars,
|
|
108
|
-
...config.variables,
|
|
109
|
-
ERROR_CODE: errorCode,
|
|
110
|
-
DESCRIPTION: config.description,
|
|
111
|
-
HTTP_STATUS: httpStatus,
|
|
112
|
-
};
|
|
113
|
-
const processedSchema = this.processTemplate(template, allVariables);
|
|
114
|
-
this.template.components.responses[httpStatus] = {
|
|
115
|
-
description: config.description,
|
|
116
|
-
content: {
|
|
117
|
-
"application/json": {
|
|
118
|
-
schema: processedSchema,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
processTemplate(template, variables) {
|
|
125
|
-
const jsonStr = JSON.stringify(template);
|
|
126
|
-
let result = jsonStr;
|
|
127
|
-
Object.entries(variables).forEach(([key, value]) => {
|
|
128
|
-
result = result.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
129
|
-
});
|
|
130
|
-
return JSON.parse(result);
|
|
131
|
-
}
|
|
132
|
-
guessHttpStatus(errorCode) {
|
|
133
|
-
const numericCode = parseInt(errorCode);
|
|
134
|
-
if (numericCode >= 100 && numericCode < 600) {
|
|
135
|
-
return numericCode;
|
|
136
|
-
}
|
|
137
|
-
const statusMap = {
|
|
138
|
-
bad: 400,
|
|
139
|
-
invalid: 400,
|
|
140
|
-
validation: 422,
|
|
141
|
-
unauthorized: 401,
|
|
142
|
-
auth: 401,
|
|
143
|
-
forbidden: 403,
|
|
144
|
-
permission: 403,
|
|
145
|
-
not_found: 404,
|
|
146
|
-
missing: 404,
|
|
147
|
-
conflict: 409,
|
|
148
|
-
duplicate: 409,
|
|
149
|
-
rate_limit: 429,
|
|
150
|
-
too_many: 429,
|
|
151
|
-
server: 500,
|
|
152
|
-
internal: 500,
|
|
153
|
-
};
|
|
154
|
-
for (const [key, status] of Object.entries(statusMap)) {
|
|
155
|
-
if (errorCode.toLowerCase().includes(key)) {
|
|
156
|
-
return status;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return 500;
|
|
160
|
-
}
|
|
161
|
-
createErrorResponseComponent(code, errorDef) {
|
|
162
|
-
return {
|
|
163
|
-
description: errorDef.description,
|
|
164
|
-
content: {
|
|
165
|
-
"application/json": {
|
|
166
|
-
schema: errorDef.schema,
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import traverseModule from "@babel/traverse";
|
|
3
|
-
const traverse = traverseModule.default || traverseModule;
|
|
4
|
-
import { HTTP_METHODS } from "./router-strategy.js";
|
|
5
|
-
import { parseTypeScriptFile, performAuthPresetReplacements } from "./utils.js";
|
|
6
|
-
export class PagesRouterStrategy {
|
|
7
|
-
config;
|
|
8
|
-
constructor(config) {
|
|
9
|
-
this.config = config;
|
|
10
|
-
}
|
|
11
|
-
shouldProcessFile(fileName) {
|
|
12
|
-
return (!fileName.startsWith("_") &&
|
|
13
|
-
(fileName.endsWith(".ts") || fileName.endsWith(".tsx")));
|
|
14
|
-
}
|
|
15
|
-
processFile(filePath, addRoute) {
|
|
16
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
17
|
-
const ast = parseTypeScriptFile(content);
|
|
18
|
-
const methodComments = [];
|
|
19
|
-
traverse(ast, {
|
|
20
|
-
ExportDefaultDeclaration: (nodePath) => {
|
|
21
|
-
const allComments = ast.comments || [];
|
|
22
|
-
const exportStart = nodePath.node.start || 0;
|
|
23
|
-
allComments.forEach((comment) => {
|
|
24
|
-
if (comment.type === "CommentBlock" &&
|
|
25
|
-
(comment.end || 0) < exportStart) {
|
|
26
|
-
const commentValue = comment.value;
|
|
27
|
-
if (commentValue.includes("@method")) {
|
|
28
|
-
const dataTypes = this.extractJSDocFromComment(commentValue);
|
|
29
|
-
if (dataTypes.method && HTTP_METHODS.includes(dataTypes.method)) {
|
|
30
|
-
methodComments.push({
|
|
31
|
-
method: dataTypes.method,
|
|
32
|
-
dataTypes,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
methodComments.forEach(({ method, dataTypes }) => {
|
|
39
|
-
addRoute(method, filePath, dataTypes);
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
getRoutePath(filePath) {
|
|
45
|
-
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
46
|
-
const normalizedApiDir = this.config.apiDir
|
|
47
|
-
.replaceAll("\\", "/")
|
|
48
|
-
.replace(/^\.\//, "")
|
|
49
|
-
.replace(/\/$/, "");
|
|
50
|
-
const apiDirIndex = normalizedPath.indexOf(normalizedApiDir);
|
|
51
|
-
if (apiDirIndex === -1) {
|
|
52
|
-
throw new Error(`Could not find apiDir "${this.config.apiDir}" in file path "${filePath}"`);
|
|
53
|
-
}
|
|
54
|
-
let relativePath = normalizedPath.substring(apiDirIndex + normalizedApiDir.length);
|
|
55
|
-
// Remove the file extension (.ts or .tsx)
|
|
56
|
-
relativePath = relativePath.replace(/\.tsx?$/, "");
|
|
57
|
-
// Remove /index suffix (pages/api/users/index.ts -> /users)
|
|
58
|
-
relativePath = relativePath.replace(/\/index$/, "");
|
|
59
|
-
if (!relativePath.startsWith("/")) {
|
|
60
|
-
relativePath = "/" + relativePath;
|
|
61
|
-
}
|
|
62
|
-
relativePath = relativePath.replace(/\/$/, "");
|
|
63
|
-
// Handle catch-all routes before dynamic routes
|
|
64
|
-
relativePath = relativePath.replace(/\/\[\.\.\.(.*?)\]/g, "/{$1}");
|
|
65
|
-
// Convert Next.js dynamic route syntax to OpenAPI parameter syntax
|
|
66
|
-
relativePath = relativePath.replace(/\/\[([^\]]+)\]/g, "/{$1}");
|
|
67
|
-
return relativePath || "/";
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Extract JSDoc data from a raw comment string (Pages Router specific)
|
|
71
|
-
*/
|
|
72
|
-
extractJSDocFromComment(commentValue) {
|
|
73
|
-
const cleanedComment = commentValue.replace(/\*\s*/g, "").trim();
|
|
74
|
-
let tag = "";
|
|
75
|
-
let summary = "";
|
|
76
|
-
let description = "";
|
|
77
|
-
let paramsType = "";
|
|
78
|
-
let pathParamsType = "";
|
|
79
|
-
let bodyType = "";
|
|
80
|
-
let auth = "";
|
|
81
|
-
let isOpenApi = cleanedComment.includes("@openapi");
|
|
82
|
-
let isIgnored = cleanedComment.includes("@ignore");
|
|
83
|
-
let deprecated = cleanedComment.includes("@deprecated");
|
|
84
|
-
let bodyDescription = "";
|
|
85
|
-
let contentType = "";
|
|
86
|
-
let responseType = "";
|
|
87
|
-
let responseDescription = "";
|
|
88
|
-
let responseSet = "";
|
|
89
|
-
let addResponses = "";
|
|
90
|
-
let successCode = "";
|
|
91
|
-
let operationId = "";
|
|
92
|
-
let method = "";
|
|
93
|
-
const methodMatch = cleanedComment.match(/@method\s+(\S+)/);
|
|
94
|
-
if (methodMatch) {
|
|
95
|
-
method = methodMatch[1].trim().toUpperCase();
|
|
96
|
-
}
|
|
97
|
-
const firstLine = cleanedComment.split("\n")[0];
|
|
98
|
-
if (!firstLine.trim().startsWith("@")) {
|
|
99
|
-
summary = firstLine.trim();
|
|
100
|
-
}
|
|
101
|
-
const descMatch = cleanedComment.match(/@description\s+(.*)/);
|
|
102
|
-
if (descMatch) {
|
|
103
|
-
description = descMatch[1].trim();
|
|
104
|
-
}
|
|
105
|
-
const tagMatch = cleanedComment.match(/@tag\s+(.*)/);
|
|
106
|
-
if (tagMatch) {
|
|
107
|
-
tag = tagMatch[1].trim();
|
|
108
|
-
}
|
|
109
|
-
const paramsMatch = cleanedComment.match(/@queryParams\s+([\w<>,\s\[\]]+)/) ||
|
|
110
|
-
cleanedComment.match(/@params\s+([\w<>,\s\[\]]+)/);
|
|
111
|
-
if (paramsMatch) {
|
|
112
|
-
paramsType = paramsMatch[1].trim();
|
|
113
|
-
}
|
|
114
|
-
const pathParamsMatch = cleanedComment.match(/@pathParams\s+([\w<>,\s\[\]]+)/);
|
|
115
|
-
if (pathParamsMatch) {
|
|
116
|
-
pathParamsType = pathParamsMatch[1].trim();
|
|
117
|
-
}
|
|
118
|
-
const bodyMatch = cleanedComment.match(/@body\s+([\w<>,\s\[\]]+)/);
|
|
119
|
-
if (bodyMatch) {
|
|
120
|
-
bodyType = bodyMatch[1].trim();
|
|
121
|
-
}
|
|
122
|
-
const bodyDescMatch = cleanedComment.match(/@bodyDescription\s+(.*)/);
|
|
123
|
-
if (bodyDescMatch) {
|
|
124
|
-
bodyDescription = bodyDescMatch[1].trim();
|
|
125
|
-
}
|
|
126
|
-
const contentTypeMatch = cleanedComment.match(/@contentType\s+(.*)/);
|
|
127
|
-
if (contentTypeMatch) {
|
|
128
|
-
contentType = contentTypeMatch[1].trim();
|
|
129
|
-
}
|
|
130
|
-
const responseMatch = cleanedComment.match(/@response\s+(?:(\d+):)?([^@\n\r]+)/);
|
|
131
|
-
if (responseMatch) {
|
|
132
|
-
const [, code, type] = responseMatch;
|
|
133
|
-
const trimmedType = type?.trim();
|
|
134
|
-
if (!code && trimmedType && /^\d{3}$/.test(trimmedType)) {
|
|
135
|
-
successCode = trimmedType;
|
|
136
|
-
responseType = undefined;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
successCode = code || "";
|
|
140
|
-
responseType = trimmedType;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const respDescMatch = cleanedComment.match(/@responseDescription\s+(.*)/);
|
|
144
|
-
if (respDescMatch) {
|
|
145
|
-
responseDescription = respDescMatch[1].trim();
|
|
146
|
-
}
|
|
147
|
-
const respSetMatch = cleanedComment.match(/@responseSet\s+(.*)/);
|
|
148
|
-
if (respSetMatch) {
|
|
149
|
-
responseSet = respSetMatch[1].trim();
|
|
150
|
-
}
|
|
151
|
-
const addMatch = cleanedComment.match(/@add\s+(.*)/);
|
|
152
|
-
if (addMatch) {
|
|
153
|
-
addResponses = addMatch[1].trim();
|
|
154
|
-
}
|
|
155
|
-
const opIdMatch = cleanedComment.match(/@operationId\s+(\S+)/);
|
|
156
|
-
if (opIdMatch) {
|
|
157
|
-
operationId = opIdMatch[1].trim();
|
|
158
|
-
}
|
|
159
|
-
const authMatch = cleanedComment.match(/@auth\s+(.*)/);
|
|
160
|
-
if (authMatch) {
|
|
161
|
-
const authValue = authMatch[1].trim();
|
|
162
|
-
switch (authValue) {
|
|
163
|
-
case "bearer":
|
|
164
|
-
auth = "BearerAuth";
|
|
165
|
-
break;
|
|
166
|
-
case "basic":
|
|
167
|
-
auth = "BasicAuth";
|
|
168
|
-
break;
|
|
169
|
-
case "apikey":
|
|
170
|
-
auth = "ApiKeyAuth";
|
|
171
|
-
break;
|
|
172
|
-
default:
|
|
173
|
-
auth = performAuthPresetReplacements(authValue);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
tag,
|
|
178
|
-
auth,
|
|
179
|
-
summary,
|
|
180
|
-
description,
|
|
181
|
-
paramsType,
|
|
182
|
-
pathParamsType,
|
|
183
|
-
bodyType,
|
|
184
|
-
isOpenApi,
|
|
185
|
-
isIgnored,
|
|
186
|
-
deprecated,
|
|
187
|
-
bodyDescription,
|
|
188
|
-
contentType,
|
|
189
|
-
responseType,
|
|
190
|
-
responseDescription,
|
|
191
|
-
responseSet,
|
|
192
|
-
addResponses,
|
|
193
|
-
successCode,
|
|
194
|
-
operationId,
|
|
195
|
-
method,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|