next-openapi-gen 0.6.4 → 0.6.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/README.md +3 -1
- package/dist/lib/logger.js +39 -0
- package/dist/lib/openapi-generator.js +8 -2
- package/dist/lib/route-processor.js +4 -2
- package/dist/lib/schema-processor.js +7 -6
- package/dist/lib/utils.js +1 -0
- package/dist/lib/zod-converter.js +22 -21
- package/dist/openapi-template.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,8 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
|
|
|
61
61
|
"schemaType": "typescript", // or "zod" for Zod schemas
|
|
62
62
|
"outputFile": "openapi.json",
|
|
63
63
|
"docsUrl": "/api-docs",
|
|
64
|
-
"includeOpenApiRoutes": false
|
|
64
|
+
"includeOpenApiRoutes": false,
|
|
65
|
+
"debug": false
|
|
65
66
|
}
|
|
66
67
|
```
|
|
67
68
|
|
|
@@ -78,6 +79,7 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
|
|
|
78
79
|
| `defaultResponseSet` | Default error response set for all endpoints |
|
|
79
80
|
| `responseSets` | Named sets of error response codes |
|
|
80
81
|
| `errorConfig` | Error schema configuration |
|
|
82
|
+
| `debug` | Enable detailed logging during generation |
|
|
81
83
|
|
|
82
84
|
## Documenting Your API
|
|
83
85
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class Logger {
|
|
2
|
+
config = null;
|
|
3
|
+
init(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
getCallerInfo() {
|
|
7
|
+
const stack = new Error().stack;
|
|
8
|
+
if (!stack)
|
|
9
|
+
return 'Unknown';
|
|
10
|
+
const lines = stack.split('\n');
|
|
11
|
+
// Skip: Error, getCallerInfo, log/warn/error
|
|
12
|
+
const callerLine = lines[3] || lines[2];
|
|
13
|
+
// Extract class/function name
|
|
14
|
+
const match = callerLine.match(/at (\w+)\.(\w+)|at (\w+)/);
|
|
15
|
+
if (match) {
|
|
16
|
+
return match[1] || match[3] || 'Unknown';
|
|
17
|
+
}
|
|
18
|
+
return 'Unknown';
|
|
19
|
+
}
|
|
20
|
+
log(message, ...args) {
|
|
21
|
+
if (this.config?.debug) {
|
|
22
|
+
const source = this.getCallerInfo();
|
|
23
|
+
console.log(`[${source}] ${message}`, ...args);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
warn(message, ...args) {
|
|
27
|
+
if (this.config?.debug) {
|
|
28
|
+
const source = this.getCallerInfo();
|
|
29
|
+
console.warn(`[${source}] ${message}`, ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
error(message, ...args) {
|
|
33
|
+
if (this.config?.debug) {
|
|
34
|
+
const source = this.getCallerInfo();
|
|
35
|
+
console.error(`[${source}] ${message}`, ...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export const logger = new Logger();
|
|
@@ -2,6 +2,7 @@ import path from "path";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { RouteProcessor } from "./route-processor.js";
|
|
4
4
|
import { cleanSpec } from "./utils.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
5
6
|
export class OpenApiGenerator {
|
|
6
7
|
config;
|
|
7
8
|
template;
|
|
@@ -11,10 +12,12 @@ export class OpenApiGenerator {
|
|
|
11
12
|
this.template = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
|
|
12
13
|
this.config = this.getConfig();
|
|
13
14
|
this.routeProcessor = new RouteProcessor(this.config);
|
|
15
|
+
// Initialize logger
|
|
16
|
+
logger.init(this.config);
|
|
14
17
|
}
|
|
15
18
|
getConfig() {
|
|
16
19
|
// @ts-ignore
|
|
17
|
-
const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig } = this.template;
|
|
20
|
+
const { apiDir, schemaDir, docsUrl, ui, outputFile, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
|
|
18
21
|
return {
|
|
19
22
|
apiDir,
|
|
20
23
|
schemaDir,
|
|
@@ -26,15 +29,17 @@ export class OpenApiGenerator {
|
|
|
26
29
|
defaultResponseSet,
|
|
27
30
|
responseSets,
|
|
28
31
|
errorConfig,
|
|
32
|
+
debug,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
generate() {
|
|
36
|
+
logger.log("Starting OpenAPI generation...");
|
|
32
37
|
const { apiDir } = this.config;
|
|
33
38
|
// Check if app router structure exists
|
|
34
39
|
let appRouterApiDir = "";
|
|
35
40
|
if (fs.existsSync(path.join(path.dirname(apiDir), "app", "api"))) {
|
|
36
41
|
appRouterApiDir = path.join(path.dirname(apiDir), "app", "api");
|
|
37
|
-
|
|
42
|
+
logger.log(`Found app router API directory at ${appRouterApiDir}`);
|
|
38
43
|
}
|
|
39
44
|
// Scan pages router routes
|
|
40
45
|
this.routeProcessor.scanApiRoutes(apiDir);
|
|
@@ -86,6 +91,7 @@ export class OpenApiGenerator {
|
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
const openapiSpec = cleanSpec(this.template);
|
|
94
|
+
logger.log("OpenAPI generation completed");
|
|
89
95
|
return openapiSpec;
|
|
90
96
|
}
|
|
91
97
|
generateErrorResponsesFromConfig(errorConfig) {
|
|
@@ -5,6 +5,7 @@ import traverse from "@babel/traverse";
|
|
|
5
5
|
import { parse } from "@babel/parser";
|
|
6
6
|
import { SchemaProcessor } from "./schema-processor.js";
|
|
7
7
|
import { capitalize, extractJSDocComments, extractPathParameters, getOperationId, } from "./utils.js";
|
|
8
|
+
import { logger } from "./logger.js";
|
|
8
9
|
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
9
10
|
const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
|
|
10
11
|
export class RouteProcessor {
|
|
@@ -136,7 +137,7 @@ export class RouteProcessor {
|
|
|
136
137
|
const pathParams = extractPathParameters(routePath);
|
|
137
138
|
// If we have path parameters but no pathParamsType defined, we should log a warning
|
|
138
139
|
if (pathParams.length > 0 && !dataTypes.pathParamsType) {
|
|
139
|
-
|
|
140
|
+
logger.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
|
|
140
141
|
}
|
|
141
142
|
this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
|
|
142
143
|
}
|
|
@@ -153,7 +154,7 @@ export class RouteProcessor {
|
|
|
153
154
|
const routePath = this.getRoutePath(filePath);
|
|
154
155
|
const pathParams = extractPathParameters(routePath);
|
|
155
156
|
if (pathParams.length > 0 && !dataTypes.pathParamsType) {
|
|
156
|
-
|
|
157
|
+
logger.warn(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
|
|
157
158
|
}
|
|
158
159
|
this.addRouteToPaths(decl.id.name, filePath, dataTypes);
|
|
159
160
|
}
|
|
@@ -166,6 +167,7 @@ export class RouteProcessor {
|
|
|
166
167
|
this.processFileTracker[filePath] = true;
|
|
167
168
|
}
|
|
168
169
|
scanApiRoutes(dir) {
|
|
170
|
+
logger.log(`Scanning API routes in: ${dir}`);
|
|
169
171
|
let files = this.directoryCache[dir];
|
|
170
172
|
if (!files) {
|
|
171
173
|
files = fs.readdirSync(dir);
|
|
@@ -4,6 +4,7 @@ import { parse } from "@babel/parser";
|
|
|
4
4
|
import traverse from "@babel/traverse";
|
|
5
5
|
import * as t from "@babel/types";
|
|
6
6
|
import { ZodSchemaConverter } from "./zod-converter.js";
|
|
7
|
+
import { logger } from "./logger.js";
|
|
7
8
|
export class SchemaProcessor {
|
|
8
9
|
schemaDir;
|
|
9
10
|
typeDefinitions = {};
|
|
@@ -42,20 +43,20 @@ export class SchemaProcessor {
|
|
|
42
43
|
this.contentType = contentType;
|
|
43
44
|
// Check if we should use Zod schemas
|
|
44
45
|
if (this.schemaType === "zod") {
|
|
45
|
-
|
|
46
|
+
logger.log(`Looking for Zod schema: ${schemaName}`);
|
|
46
47
|
// Check type mapping first
|
|
47
48
|
const mappedSchemaName = this.zodSchemaConverter.typeToSchemaMapping[schemaName];
|
|
48
49
|
if (mappedSchemaName) {
|
|
49
|
-
|
|
50
|
+
logger.log(`Type '${schemaName}' is mapped to Zod schema '${mappedSchemaName}'`);
|
|
50
51
|
}
|
|
51
52
|
// Try to convert Zod schema
|
|
52
53
|
const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(schemaName);
|
|
53
54
|
if (zodSchema) {
|
|
54
|
-
|
|
55
|
+
logger.log(`Found and processed Zod schema: ${schemaName}`);
|
|
55
56
|
this.openapiDefinitions[schemaName] = zodSchema;
|
|
56
57
|
return zodSchema;
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
logger.log(`No Zod schema found for ${schemaName}, trying TypeScript fallback`);
|
|
59
60
|
}
|
|
60
61
|
// Fall back to TypeScript types
|
|
61
62
|
this.scanSchemaDir(this.schemaDir, schemaName);
|
|
@@ -394,7 +395,7 @@ export class SchemaProcessor {
|
|
|
394
395
|
if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
|
|
395
396
|
return { $ref: `#/components/schemas/${node.typeName.name}` };
|
|
396
397
|
}
|
|
397
|
-
|
|
398
|
+
logger.log("Unrecognized TypeScript type node:", node);
|
|
398
399
|
return { type: "object" }; // By default we return an object
|
|
399
400
|
}
|
|
400
401
|
processSchemaFile(filePath, schemaName) {
|
|
@@ -417,7 +418,7 @@ export class SchemaProcessor {
|
|
|
417
418
|
return definition;
|
|
418
419
|
}
|
|
419
420
|
catch (error) {
|
|
420
|
-
|
|
421
|
+
logger.error(`Error processing schema file ${filePath} for schema ${schemaName}: ${error}`);
|
|
421
422
|
return { type: "object" }; // By default we return an empty object on error
|
|
422
423
|
}
|
|
423
424
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -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 { logger } from "./logger.js";
|
|
6
7
|
/**
|
|
7
8
|
* Class for converting Zod schemas to OpenAPI specifications
|
|
8
9
|
*/
|
|
@@ -23,11 +24,11 @@ export class ZodSchemaConverter {
|
|
|
23
24
|
if (Object.keys(this.typeToSchemaMapping).length === 0) {
|
|
24
25
|
this.preScanForTypeMappings();
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
logger.log(`Looking for Zod schema: ${schemaName}`);
|
|
27
28
|
// Check mapped types
|
|
28
29
|
const mappedSchemaName = this.typeToSchemaMapping[schemaName];
|
|
29
30
|
if (mappedSchemaName) {
|
|
30
|
-
|
|
31
|
+
logger.log(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
|
|
31
32
|
schemaName = mappedSchemaName;
|
|
32
33
|
}
|
|
33
34
|
// Check for circular references
|
|
@@ -46,7 +47,7 @@ export class ZodSchemaConverter {
|
|
|
46
47
|
for (const routeFile of routeFiles) {
|
|
47
48
|
this.processFileForZodSchema(routeFile, schemaName);
|
|
48
49
|
if (this.zodSchemas[schemaName]) {
|
|
49
|
-
|
|
50
|
+
logger.log(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
|
|
50
51
|
return this.zodSchemas[schemaName];
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -54,10 +55,10 @@ export class ZodSchemaConverter {
|
|
|
54
55
|
this.scanDirectoryForZodSchema(this.schemaDir, schemaName);
|
|
55
56
|
// Return the schema if found, or null if not
|
|
56
57
|
if (this.zodSchemas[schemaName]) {
|
|
57
|
-
|
|
58
|
+
logger.log(`Found and processed Zod schema: ${schemaName}`);
|
|
58
59
|
return this.zodSchemas[schemaName];
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
logger.log(`Could not find Zod schema: ${schemaName}`);
|
|
61
62
|
return null;
|
|
62
63
|
}
|
|
63
64
|
finally {
|
|
@@ -104,7 +105,7 @@ export class ZodSchemaConverter {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
catch (error) {
|
|
107
|
-
|
|
108
|
+
logger.log(`Error scanning directory ${dir} for route files: ${error}`);
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
/**
|
|
@@ -125,7 +126,7 @@ export class ZodSchemaConverter {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
catch (error) {
|
|
128
|
-
|
|
129
|
+
logger.log(`Error scanning directory ${dir}: ${error}`);
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
/**
|
|
@@ -272,7 +273,7 @@ export class ZodSchemaConverter {
|
|
|
272
273
|
? prop.key.value
|
|
273
274
|
: null;
|
|
274
275
|
if (key && schema.properties) {
|
|
275
|
-
|
|
276
|
+
logger.log(`Removing property: ${key}`);
|
|
276
277
|
delete schema.properties[key];
|
|
277
278
|
if (schema.required) {
|
|
278
279
|
schema.required = schema.required.filter((r) => r !== key);
|
|
@@ -357,20 +358,20 @@ export class ZodSchemaConverter {
|
|
|
357
358
|
if (t.isCallExpression(path.node.init)) {
|
|
358
359
|
const baseSchemaName = findBaseSchema(path.node.init);
|
|
359
360
|
if (baseSchemaName && baseSchemaName !== "z") {
|
|
360
|
-
|
|
361
|
+
logger.log(`Found chained call starting from: ${baseSchemaName}`);
|
|
361
362
|
// First make sure the underlying schema is processed
|
|
362
363
|
if (!this.zodSchemas[baseSchemaName]) {
|
|
363
|
-
|
|
364
|
+
logger.log(`Base schema ${baseSchemaName} not found, processing it first`);
|
|
364
365
|
this.processFileForZodSchema(filePath, baseSchemaName);
|
|
365
366
|
}
|
|
366
367
|
if (this.zodSchemas[baseSchemaName]) {
|
|
367
|
-
|
|
368
|
+
logger.log("Base schema found, applying transformations");
|
|
368
369
|
// Copy base schema
|
|
369
370
|
const baseSchema = JSON.parse(JSON.stringify(this.zodSchemas[baseSchemaName]));
|
|
370
371
|
// Process the entire call chain
|
|
371
372
|
const finalSchema = processChainedCall(path.node.init, baseSchema);
|
|
372
373
|
this.zodSchemas[schemaName] = finalSchema;
|
|
373
|
-
|
|
374
|
+
logger.log(`Created ${schemaName} with properties: ${Object.keys(finalSchema.properties || {})}`);
|
|
374
375
|
return;
|
|
375
376
|
}
|
|
376
377
|
}
|
|
@@ -412,7 +413,7 @@ export class ZodSchemaConverter {
|
|
|
412
413
|
const referencedSchemaName = param.exprName.name;
|
|
413
414
|
// Save mapping: TypeName -> SchemaName
|
|
414
415
|
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
415
|
-
|
|
416
|
+
logger.log(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
416
417
|
// Process the referenced schema if not already processed
|
|
417
418
|
if (!this.zodSchemas[referencedSchemaName]) {
|
|
418
419
|
this.processFileForZodSchema(filePath, referencedSchemaName);
|
|
@@ -1222,7 +1223,7 @@ export class ZodSchemaConverter {
|
|
|
1222
1223
|
schema.description = baseSchema.description;
|
|
1223
1224
|
}
|
|
1224
1225
|
else {
|
|
1225
|
-
|
|
1226
|
+
logger.warn("Could not resolve base schema for extend");
|
|
1226
1227
|
schema = extendedProps || { type: "object" };
|
|
1227
1228
|
}
|
|
1228
1229
|
}
|
|
@@ -1313,7 +1314,7 @@ export class ZodSchemaConverter {
|
|
|
1313
1314
|
* Pre-scan all files to build type mappings
|
|
1314
1315
|
*/
|
|
1315
1316
|
preScanForTypeMappings() {
|
|
1316
|
-
|
|
1317
|
+
logger.log("Pre-scanning for type mappings...");
|
|
1317
1318
|
// Scan route files
|
|
1318
1319
|
const routeFiles = this.findRouteFiles();
|
|
1319
1320
|
for (const routeFile of routeFiles) {
|
|
@@ -1359,7 +1360,7 @@ export class ZodSchemaConverter {
|
|
|
1359
1360
|
if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
|
|
1360
1361
|
const referencedSchemaName = param.exprName.name;
|
|
1361
1362
|
this.typeToSchemaMapping[typeName] = referencedSchemaName;
|
|
1362
|
-
|
|
1363
|
+
logger.log(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
|
|
1363
1364
|
}
|
|
1364
1365
|
}
|
|
1365
1366
|
}
|
|
@@ -1368,7 +1369,7 @@ export class ZodSchemaConverter {
|
|
|
1368
1369
|
});
|
|
1369
1370
|
}
|
|
1370
1371
|
catch (error) {
|
|
1371
|
-
|
|
1372
|
+
logger.log(`Error scanning file ${filePath} for type mappings: ${error}`);
|
|
1372
1373
|
}
|
|
1373
1374
|
}
|
|
1374
1375
|
/**
|
|
@@ -1389,7 +1390,7 @@ export class ZodSchemaConverter {
|
|
|
1389
1390
|
}
|
|
1390
1391
|
}
|
|
1391
1392
|
catch (error) {
|
|
1392
|
-
|
|
1393
|
+
logger.error(`Error scanning directory ${dir} for type mappings: ${error}`);
|
|
1393
1394
|
}
|
|
1394
1395
|
}
|
|
1395
1396
|
/**
|
|
@@ -1412,7 +1413,7 @@ export class ZodSchemaConverter {
|
|
|
1412
1413
|
// Check if is Zos schema
|
|
1413
1414
|
if (this.isZodSchema(declaration.init) &&
|
|
1414
1415
|
!this.zodSchemas[schemaName]) {
|
|
1415
|
-
|
|
1416
|
+
logger.log(`Pre-processing Zod schema: ${schemaName}`);
|
|
1416
1417
|
this.processingSchemas.add(schemaName);
|
|
1417
1418
|
const schema = this.processZodNode(declaration.init);
|
|
1418
1419
|
if (schema) {
|
|
@@ -1432,7 +1433,7 @@ export class ZodSchemaConverter {
|
|
|
1432
1433
|
if (this.isZodSchema(declaration.init) &&
|
|
1433
1434
|
!this.zodSchemas[schemaName] &&
|
|
1434
1435
|
!this.processingSchemas.has(schemaName)) {
|
|
1435
|
-
|
|
1436
|
+
logger.log(`Pre-processing Zod schema: ${schemaName}`);
|
|
1436
1437
|
this.processingSchemas.add(schemaName);
|
|
1437
1438
|
const schema = this.processZodNode(declaration.init);
|
|
1438
1439
|
if (schema) {
|
|
@@ -1446,7 +1447,7 @@ export class ZodSchemaConverter {
|
|
|
1446
1447
|
});
|
|
1447
1448
|
}
|
|
1448
1449
|
catch (error) {
|
|
1449
|
-
|
|
1450
|
+
logger.error(`Error pre-processing file ${filePath}: ${error}`);
|
|
1450
1451
|
}
|
|
1451
1452
|
}
|
|
1452
1453
|
/**
|
package/dist/openapi-template.js
CHANGED
package/package.json
CHANGED