next-openapi-gen 0.3.2 → 0.4.1
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/README.md +331 -151
- package/dist/components/scalar.js +16 -16
- package/dist/lib/openapi-generator.js +42 -3
- package/dist/lib/route-processor.js +23 -3
- package/dist/lib/schema-processor.js +88 -17
- package/dist/lib/utils.js +14 -14
- package/dist/lib/zod-converter.js +916 -0
- package/dist/openapi-template.js +1 -0
- package/package.json +8 -5
|
@@ -18,6 +18,12 @@ export class RouteProcessor {
|
|
|
18
18
|
this.config = config;
|
|
19
19
|
this.schemaProcessor = new SchemaProcessor(config.schemaDir);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the SchemaProcessor instance
|
|
23
|
+
*/
|
|
24
|
+
getSchemaProcessor() {
|
|
25
|
+
return this.schemaProcessor;
|
|
26
|
+
}
|
|
21
27
|
isRoute(varName) {
|
|
22
28
|
return HTTP_METHODS.includes(varName);
|
|
23
29
|
}
|
|
@@ -39,8 +45,16 @@ export class RouteProcessor {
|
|
|
39
45
|
if (this.isRoute(declaration.id.name)) {
|
|
40
46
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
41
47
|
if (!this.config.includeOpenApiRoutes ||
|
|
42
|
-
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
|
|
48
|
+
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
|
|
49
|
+
// Check for URL parameters in the route path
|
|
50
|
+
const routePath = this.getRoutePath(filePath);
|
|
51
|
+
const pathParams = extractPathParameters(routePath);
|
|
52
|
+
// If we have path parameters but no pathParamsType defined, we should log a warning
|
|
53
|
+
if (pathParams.length > 0 && !dataTypes.pathParams) {
|
|
54
|
+
console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
|
|
55
|
+
}
|
|
43
56
|
this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
if (t.isVariableDeclaration(declaration)) {
|
|
@@ -50,8 +64,14 @@ export class RouteProcessor {
|
|
|
50
64
|
const dataTypes = extractJSDocComments(path);
|
|
51
65
|
// Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
|
|
52
66
|
if (!this.config.includeOpenApiRoutes ||
|
|
53
|
-
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
|
|
67
|
+
(this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
|
|
68
|
+
const routePath = this.getRoutePath(filePath);
|
|
69
|
+
const pathParams = extractPathParameters(routePath);
|
|
70
|
+
if (pathParams.length > 0 && !dataTypes.pathParams) {
|
|
71
|
+
console.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
|
|
72
|
+
}
|
|
54
73
|
this.addRouteToPaths(decl.id.name, filePath, dataTypes);
|
|
74
|
+
}
|
|
55
75
|
}
|
|
56
76
|
}
|
|
57
77
|
});
|
|
@@ -103,6 +123,7 @@ export class RouteProcessor {
|
|
|
103
123
|
summary: summary,
|
|
104
124
|
description: description,
|
|
105
125
|
tags: [rootPath],
|
|
126
|
+
parameters: [],
|
|
106
127
|
};
|
|
107
128
|
// Add auth
|
|
108
129
|
if (auth) {
|
|
@@ -112,7 +133,6 @@ export class RouteProcessor {
|
|
|
112
133
|
},
|
|
113
134
|
];
|
|
114
135
|
}
|
|
115
|
-
definition.parameters = [];
|
|
116
136
|
if (params) {
|
|
117
137
|
definition.parameters =
|
|
118
138
|
this.schemaProcessor.createRequestParamsSchema(params);
|
|
@@ -3,6 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { parse } from "@babel/parser";
|
|
4
4
|
import traverse from "@babel/traverse";
|
|
5
5
|
import * as t from "@babel/types";
|
|
6
|
+
import { ZodSchemaConverter } from "./zod-converter";
|
|
6
7
|
export class SchemaProcessor {
|
|
7
8
|
schemaDir;
|
|
8
9
|
typeDefinitions = {};
|
|
@@ -12,13 +13,38 @@ export class SchemaProcessor {
|
|
|
12
13
|
statCache = {};
|
|
13
14
|
processSchemaTracker = {};
|
|
14
15
|
processingTypes = new Set();
|
|
15
|
-
|
|
16
|
+
zodSchemaConverter;
|
|
17
|
+
schemaType;
|
|
18
|
+
constructor(schemaDir, schemaType = "typescript") {
|
|
16
19
|
this.schemaDir = path.resolve(schemaDir);
|
|
20
|
+
this.schemaType = schemaType;
|
|
21
|
+
if (schemaType === "zod") {
|
|
22
|
+
this.zodSchemaConverter = new ZodSchemaConverter(schemaDir);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get all defined schemas (for components.schemas section)
|
|
27
|
+
*/
|
|
28
|
+
getDefinedSchemas() {
|
|
29
|
+
return this.openapiDefinitions;
|
|
17
30
|
}
|
|
18
31
|
findSchemaDefinition(schemaName, contentType) {
|
|
19
32
|
let schemaNode = null;
|
|
20
|
-
//
|
|
33
|
+
// Assign type that is actually processed
|
|
21
34
|
this.contentType = contentType;
|
|
35
|
+
// Check if we should use Zod schemas
|
|
36
|
+
if (this.schemaType === "zod") {
|
|
37
|
+
console.log(`Looking for Zod schema: ${schemaName}`);
|
|
38
|
+
// Try to convert Zod schema
|
|
39
|
+
const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(schemaName);
|
|
40
|
+
if (zodSchema) {
|
|
41
|
+
console.log(`Found and processed Zod schema: ${schemaName}`);
|
|
42
|
+
this.openapiDefinitions[schemaName] = zodSchema;
|
|
43
|
+
return zodSchema;
|
|
44
|
+
}
|
|
45
|
+
console.log(`No Zod schema found for ${schemaName}, trying TypeScript fallback`);
|
|
46
|
+
}
|
|
47
|
+
// Fall back to TypeScript types
|
|
22
48
|
this.scanSchemaDir(this.schemaDir, schemaName);
|
|
23
49
|
return schemaNode;
|
|
24
50
|
}
|
|
@@ -38,7 +64,7 @@ export class SchemaProcessor {
|
|
|
38
64
|
if (stat.isDirectory()) {
|
|
39
65
|
this.scanSchemaDir(filePath, schemaName);
|
|
40
66
|
}
|
|
41
|
-
else if (file.endsWith(".ts")) {
|
|
67
|
+
else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
42
68
|
this.processSchemaFile(filePath, schemaName);
|
|
43
69
|
}
|
|
44
70
|
});
|
|
@@ -69,6 +95,25 @@ export class SchemaProcessor {
|
|
|
69
95
|
this.typeDefinitions[name] = path.node;
|
|
70
96
|
}
|
|
71
97
|
},
|
|
98
|
+
// Collect exported zod schemas
|
|
99
|
+
ExportNamedDeclaration: (path) => {
|
|
100
|
+
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
101
|
+
path.node.declaration.declarations.forEach((declaration) => {
|
|
102
|
+
if (t.isIdentifier(declaration.id) &&
|
|
103
|
+
declaration.id.name === schemaName &&
|
|
104
|
+
declaration.init) {
|
|
105
|
+
// Check if is Zod schema
|
|
106
|
+
if (t.isCallExpression(declaration.init) &&
|
|
107
|
+
t.isMemberExpression(declaration.init.callee) &&
|
|
108
|
+
t.isIdentifier(declaration.init.callee.object) &&
|
|
109
|
+
declaration.init.callee.object.name === "z") {
|
|
110
|
+
const name = declaration.id.name;
|
|
111
|
+
this.typeDefinitions[name] = declaration.init;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
72
117
|
});
|
|
73
118
|
}
|
|
74
119
|
resolveType(typeName) {
|
|
@@ -76,12 +121,33 @@ export class SchemaProcessor {
|
|
|
76
121
|
// Return reference to type to avoid infinite recursion
|
|
77
122
|
return { $ref: `#/components/schemas/${typeName}` };
|
|
78
123
|
}
|
|
79
|
-
// Add type to
|
|
124
|
+
// Add type to processing types
|
|
80
125
|
this.processingTypes.add(typeName);
|
|
81
126
|
try {
|
|
127
|
+
// If we are using Zod and the given type is not found yet, try using Zod converter first
|
|
128
|
+
if (this.schemaType === "zod" && !this.openapiDefinitions[typeName]) {
|
|
129
|
+
const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(typeName);
|
|
130
|
+
if (zodSchema) {
|
|
131
|
+
this.openapiDefinitions[typeName] = zodSchema;
|
|
132
|
+
return zodSchema;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
82
135
|
const typeNode = this.typeDefinitions[typeName.toString()];
|
|
83
136
|
if (!typeNode)
|
|
84
137
|
return {};
|
|
138
|
+
// Check if node is Zod
|
|
139
|
+
if (t.isCallExpression(typeNode) &&
|
|
140
|
+
t.isMemberExpression(typeNode.callee) &&
|
|
141
|
+
t.isIdentifier(typeNode.callee.object) &&
|
|
142
|
+
typeNode.callee.object.name === "z") {
|
|
143
|
+
if (this.schemaType === "zod") {
|
|
144
|
+
const zodSchema = this.zodSchemaConverter.processZodNode(typeNode);
|
|
145
|
+
if (zodSchema) {
|
|
146
|
+
this.openapiDefinitions[typeName] = zodSchema;
|
|
147
|
+
return zodSchema;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
85
151
|
if (t.isTSEnumDeclaration(typeNode)) {
|
|
86
152
|
const enumValues = this.processEnum(typeNode);
|
|
87
153
|
return enumValues;
|
|
@@ -296,7 +362,6 @@ export class SchemaProcessor {
|
|
|
296
362
|
if (resolvedType.type === "object" && resolvedType.properties) {
|
|
297
363
|
Object.entries(resolvedType.properties).forEach(([key, value]) => {
|
|
298
364
|
allProperties[key] = value;
|
|
299
|
-
// @ts-ignore
|
|
300
365
|
if (value.required) {
|
|
301
366
|
requiredProperties.push(key);
|
|
302
367
|
}
|
|
@@ -358,7 +423,9 @@ export class SchemaProcessor {
|
|
|
358
423
|
enumSchema.type = "number";
|
|
359
424
|
}
|
|
360
425
|
const targetValue = value || name;
|
|
361
|
-
enumSchema.enum
|
|
426
|
+
if (enumSchema.enum) {
|
|
427
|
+
enumSchema.enum.push(targetValue);
|
|
428
|
+
}
|
|
362
429
|
}
|
|
363
430
|
});
|
|
364
431
|
return enumSchema;
|
|
@@ -390,12 +457,12 @@ export class SchemaProcessor {
|
|
|
390
457
|
if (paramName === "id" ||
|
|
391
458
|
paramName.endsWith("Id") ||
|
|
392
459
|
paramName.endsWith("_id")) {
|
|
393
|
-
return type === "string" ? "
|
|
460
|
+
return type === "string" ? "123" : 123;
|
|
394
461
|
}
|
|
395
462
|
// For specific common parameter names
|
|
396
463
|
switch (paramName.toLowerCase()) {
|
|
397
464
|
case "slug":
|
|
398
|
-
return "
|
|
465
|
+
return "slug";
|
|
399
466
|
case "uuid":
|
|
400
467
|
return "123e4567-e89b-12d3-a456-426614174000";
|
|
401
468
|
case "username":
|
|
@@ -403,11 +470,13 @@ export class SchemaProcessor {
|
|
|
403
470
|
case "email":
|
|
404
471
|
return "user@example.com";
|
|
405
472
|
case "name":
|
|
406
|
-
return "
|
|
473
|
+
return "name";
|
|
407
474
|
case "date":
|
|
408
475
|
return "2023-01-01";
|
|
409
476
|
case "page":
|
|
410
477
|
return 1;
|
|
478
|
+
case "role":
|
|
479
|
+
return "admin";
|
|
411
480
|
default:
|
|
412
481
|
// Default examples by type
|
|
413
482
|
if (type === "string")
|
|
@@ -498,25 +567,27 @@ export class SchemaProcessor {
|
|
|
498
567
|
};
|
|
499
568
|
}
|
|
500
569
|
getSchemaContent({ paramsType, pathParamsType, bodyType, responseType, }) {
|
|
501
|
-
let params = this.openapiDefinitions[paramsType];
|
|
502
|
-
let pathParams =
|
|
503
|
-
|
|
504
|
-
|
|
570
|
+
let params = paramsType ? this.openapiDefinitions[paramsType] : {};
|
|
571
|
+
let pathParams = pathParamsType
|
|
572
|
+
? this.openapiDefinitions[pathParamsType]
|
|
573
|
+
: {};
|
|
574
|
+
let body = bodyType ? this.openapiDefinitions[bodyType] : {};
|
|
575
|
+
let responses = responseType ? this.openapiDefinitions[responseType] : {};
|
|
505
576
|
if (paramsType && !params) {
|
|
506
577
|
this.findSchemaDefinition(paramsType, "params");
|
|
507
|
-
params = this.openapiDefinitions[paramsType];
|
|
578
|
+
params = this.openapiDefinitions[paramsType] || {};
|
|
508
579
|
}
|
|
509
580
|
if (pathParamsType && !pathParams) {
|
|
510
581
|
this.findSchemaDefinition(pathParamsType, "pathParams");
|
|
511
|
-
pathParams = this.openapiDefinitions[pathParamsType];
|
|
582
|
+
pathParams = this.openapiDefinitions[pathParamsType] || {};
|
|
512
583
|
}
|
|
513
584
|
if (bodyType && !body) {
|
|
514
585
|
this.findSchemaDefinition(bodyType, "body");
|
|
515
|
-
body = this.openapiDefinitions[bodyType];
|
|
586
|
+
body = this.openapiDefinitions[bodyType] || {};
|
|
516
587
|
}
|
|
517
588
|
if (responseType && !responses) {
|
|
518
589
|
this.findSchemaDefinition(responseType, "response");
|
|
519
|
-
responses = this.openapiDefinitions[responseType];
|
|
590
|
+
responses = this.openapiDefinitions[responseType] || {};
|
|
520
591
|
}
|
|
521
592
|
return {
|
|
522
593
|
params,
|
package/dist/lib/utils.js
CHANGED
|
@@ -18,10 +18,10 @@ export function extractJSDocComments(path) {
|
|
|
18
18
|
const comments = path.node.leadingComments;
|
|
19
19
|
let summary = "";
|
|
20
20
|
let description = "";
|
|
21
|
-
let
|
|
22
|
-
let
|
|
23
|
-
let
|
|
24
|
-
let
|
|
21
|
+
let params = "";
|
|
22
|
+
let pathParams = "";
|
|
23
|
+
let body = "";
|
|
24
|
+
let response = "";
|
|
25
25
|
let auth = "";
|
|
26
26
|
let isOpenApi = false;
|
|
27
27
|
if (comments) {
|
|
@@ -32,7 +32,7 @@ export function extractJSDocComments(path) {
|
|
|
32
32
|
summary = commentValue.split("\n")[0];
|
|
33
33
|
}
|
|
34
34
|
if (commentValue.includes("@auth")) {
|
|
35
|
-
const regex = /@auth
|
|
35
|
+
const regex = /@auth\s*(.*)/;
|
|
36
36
|
const value = commentValue.match(regex)[1].trim();
|
|
37
37
|
switch (value) {
|
|
38
38
|
case "bearer":
|
|
@@ -47,20 +47,20 @@ export function extractJSDocComments(path) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
if (commentValue.includes("@desc")) {
|
|
50
|
-
const regex = /@desc
|
|
50
|
+
const regex = /@desc\s*(.*)/;
|
|
51
51
|
description = commentValue.match(regex)[1].trim();
|
|
52
52
|
}
|
|
53
53
|
if (commentValue.includes("@params")) {
|
|
54
|
-
|
|
54
|
+
params = extractTypeFromComment(commentValue, "@params");
|
|
55
55
|
}
|
|
56
56
|
if (commentValue.includes("@pathParams")) {
|
|
57
|
-
|
|
57
|
+
pathParams = extractTypeFromComment(commentValue, "@pathParams");
|
|
58
58
|
}
|
|
59
59
|
if (commentValue.includes("@body")) {
|
|
60
|
-
|
|
60
|
+
body = extractTypeFromComment(commentValue, "@body");
|
|
61
61
|
}
|
|
62
62
|
if (commentValue.includes("@response")) {
|
|
63
|
-
|
|
63
|
+
response = extractTypeFromComment(commentValue, "@response");
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
}
|
|
@@ -68,10 +68,10 @@ export function extractJSDocComments(path) {
|
|
|
68
68
|
auth,
|
|
69
69
|
summary,
|
|
70
70
|
description,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
params,
|
|
72
|
+
pathParams,
|
|
73
|
+
body,
|
|
74
|
+
response,
|
|
75
75
|
isOpenApi,
|
|
76
76
|
};
|
|
77
77
|
}
|