next-openapi-gen 0.7.8 → 0.7.10

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 CHANGED
@@ -63,25 +63,27 @@ During initialization (`npx next-openapi-gen init`), a configuration file `next.
63
63
  "outputDir": "./public",
64
64
  "docsUrl": "/api-docs",
65
65
  "includeOpenApiRoutes": false,
66
+ "ignoreRoutes": [],
66
67
  "debug": false
67
68
  }
68
69
  ```
69
70
 
70
71
  ### Configuration Options
71
72
 
72
- | Option | Description |
73
- | ---------------------- | ---------------------------------------------------------------------- |
74
- | `apiDir` | Path to the API directory |
75
- | `schemaDir` | Path to the types/schemas directory |
76
- | `schemaType` | Schema type: `"zod"` or `"typescript"` |
77
- | `outputFile` | Name of the OpenAPI output file |
78
- | `outputDir` | Directory where OpenAPI file will be generated (default: `"./public"`) |
79
- | `docsUrl` | API documentation URL (for Swagger UI) |
80
- | `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
81
- | `defaultResponseSet` | Default error response set for all endpoints |
82
- | `responseSets` | Named sets of error response codes |
83
- | `errorConfig` | Error schema configuration |
84
- | `debug` | Enable detailed logging during generation |
73
+ | Option | Description |
74
+ | ---------------------- | -------------------------------------------------------------------------- |
75
+ | `apiDir` | Path to the API directory |
76
+ | `schemaDir` | Path to the types/schemas directory |
77
+ | `schemaType` | Schema type: `"zod"` or `"typescript"` |
78
+ | `outputFile` | Name of the OpenAPI output file |
79
+ | `outputDir` | Directory where OpenAPI file will be generated (default: `"./public"`) |
80
+ | `docsUrl` | API documentation URL (for Swagger UI) |
81
+ | `includeOpenApiRoutes` | Whether to include only routes with @openapi tag |
82
+ | `ignoreRoutes` | Array of route patterns to exclude from documentation (supports wildcards) |
83
+ | `defaultResponseSet` | Default error response set for all endpoints |
84
+ | `responseSets` | Named sets of error response codes |
85
+ | `errorConfig` | Error schema configuration |
86
+ | `debug` | Enable detailed logging during generation |
85
87
 
86
88
  ## Documenting Your API
87
89
 
@@ -168,6 +170,7 @@ export async function GET(
168
170
  | `@tag` | Custom tag |
169
171
  | `@deprecated` | Marks the route as deprecated |
170
172
  | `@openapi` | Marks the route for inclusion in documentation (if includeOpenApiRoutes is enabled) |
173
+ | `@ignore` | Excludes the route from OpenAPI documentation |
171
174
 
172
175
  ## CLI Usage
173
176
 
@@ -537,6 +540,48 @@ export async function PUT() {}
537
540
  }
538
541
  ```
539
542
 
543
+ ## Ignoring Routes
544
+
545
+ You can exclude routes from OpenAPI documentation in two ways:
546
+
547
+ ### Using @ignore Tag
548
+
549
+ Add the `@ignore` tag to any route you want to exclude:
550
+
551
+ ```typescript
552
+ // src/app/api/internal/route.ts
553
+
554
+ /**
555
+ * Internal route - not for documentation
556
+ * @ignore
557
+ */
558
+ export async function GET() {
559
+ // This route will not appear in OpenAPI documentation
560
+ }
561
+ ```
562
+
563
+ ### Using ignoreRoutes Configuration
564
+
565
+ Add patterns to your `next.openapi.json` configuration file to exclude multiple routes at once:
566
+
567
+ ```json
568
+ {
569
+ "openapi": "3.0.0",
570
+ "info": {
571
+ "title": "Next.js API",
572
+ "version": "1.0.0"
573
+ },
574
+ "apiDir": "src/app/api",
575
+ "ignoreRoutes": ["/internal/*", "/debug", "/admin/test/*"]
576
+ }
577
+ ```
578
+
579
+ Pattern matching supports wildcards:
580
+
581
+ - `/internal/*` - Ignores all routes under `/internal/`
582
+ - `/debug` - Ignores only the `/debug` route
583
+ - `/admin/*/temp` - Ignores routes like `/admin/users/temp`, `/admin/posts/temp`
584
+
540
585
  ## Advanced Usage
541
586
 
542
587
  ### Automatic Path Parameter Detection
@@ -554,6 +599,53 @@ export async function GET() {
554
599
 
555
600
  If no type/schema is provided for path parameters, a default schema will be generated.
556
601
 
602
+ ### TypeScript Generics Support
603
+
604
+ The library supports TypeScript generic types and automatically resolves them during documentation generation:
605
+
606
+ ```typescript
607
+ // src/app/api/llms/route.ts
608
+
609
+ import { NextResponse } from "next/server";
610
+
611
+ // Define generic response wrapper
612
+ type MyApiSuccessResponseBody<T> = T & {
613
+ success: true;
614
+ httpCode: string;
615
+ };
616
+
617
+ // Define specific response data
618
+ type LLMSResponse = {
619
+ llms: Array<{
620
+ id: string;
621
+ name: string;
622
+ provider: string;
623
+ isDefault: boolean;
624
+ }>;
625
+ };
626
+
627
+ /**
628
+ * Get list of available LLMs
629
+ * @description Get list of available LLMs with success wrapper
630
+ * @response 200:MyApiSuccessResponseBody<LLMSResponse>
631
+ * @openapi
632
+ */
633
+ export async function GET() {
634
+ return NextResponse.json({
635
+ success: true,
636
+ httpCode: "200",
637
+ llms: [
638
+ {
639
+ id: "gpt-5",
640
+ name: "GPT-5",
641
+ provider: "OpenAI",
642
+ isDefault: true,
643
+ },
644
+ ],
645
+ });
646
+ }
647
+ ```
648
+
557
649
  ### Intelligent Examples
558
650
 
559
651
  The library generates intelligent examples for parameters based on their name:
@@ -17,7 +17,7 @@ export class OpenApiGenerator {
17
17
  }
18
18
  getConfig() {
19
19
  // @ts-ignore
20
- const { apiDir, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
20
+ const { apiDir, schemaDir, docsUrl, ui, outputFile, outputDir, includeOpenApiRoutes, ignoreRoutes, schemaType = "typescript", defaultResponseSet, responseSets, errorConfig, debug } = this.template;
21
21
  return {
22
22
  apiDir: apiDir || "./src/app/api",
23
23
  schemaDir: schemaDir || "./src",
@@ -26,6 +26,7 @@ export class OpenApiGenerator {
26
26
  outputFile: outputFile || "openapi.json",
27
27
  outputDir: outputDir || "./public",
28
28
  includeOpenApiRoutes: includeOpenApiRoutes || false,
29
+ ignoreRoutes: ignoreRoutes || [],
29
30
  schemaType,
30
31
  defaultResponseSet,
31
32
  responseSets,
@@ -1,7 +1,9 @@
1
1
  import * as t from "@babel/types";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import traverse from "@babel/traverse";
4
+ import traverseModule from "@babel/traverse";
5
+ // Handle both ES modules and CommonJS
6
+ const traverse = traverseModule.default || traverseModule;
5
7
  import { SchemaProcessor } from "./schema-processor.js";
6
8
  import { capitalize, extractJSDocComments, parseTypeScriptFile, extractPathParameters, getOperationId, } from "./utils.js";
7
9
  import { logger } from "./logger.js";
@@ -23,14 +25,15 @@ export class RouteProcessor {
23
25
  // 1. Add success response
24
26
  const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
25
27
  if (dataTypes.responseType) {
26
- const responseSchema = this.schemaProcessor.getSchemaContent({
28
+ // Ensure the schema is defined in components/schemas
29
+ this.schemaProcessor.getSchemaContent({
27
30
  responseType: dataTypes.responseType,
28
- }).responses;
31
+ });
29
32
  responses[successCode] = {
30
33
  description: dataTypes.responseDescription || "Successful response",
31
34
  content: {
32
35
  "application/json": {
33
- schema: responseSchema,
36
+ schema: { $ref: `#/components/schemas/${dataTypes.responseType}` },
34
37
  },
35
38
  },
36
39
  };
@@ -112,24 +115,49 @@ export class RouteProcessor {
112
115
  isRoute(varName) {
113
116
  return HTTP_METHODS.includes(varName);
114
117
  }
118
+ /**
119
+ * Check if a route should be ignored based on config patterns or @ignore tag
120
+ */
121
+ shouldIgnoreRoute(routePath, dataTypes) {
122
+ // Check if route has @ignore tag
123
+ if (dataTypes.isIgnored) {
124
+ return true;
125
+ }
126
+ // Check if route matches any ignore patterns
127
+ const ignorePatterns = this.config.ignoreRoutes || [];
128
+ if (ignorePatterns.length === 0) {
129
+ return false;
130
+ }
131
+ return ignorePatterns.some((pattern) => {
132
+ // Support wildcards
133
+ const regexPattern = pattern.replace(/\*/g, ".*").replace(/\//g, "\\/");
134
+ const regex = new RegExp(`^${regexPattern}$`);
135
+ return regex.test(routePath);
136
+ });
137
+ }
115
138
  processFile(filePath) {
116
139
  // Check if the file has already been processed
117
140
  if (this.processFileTracker[filePath])
118
141
  return;
119
142
  const content = fs.readFileSync(filePath, "utf-8");
120
143
  const ast = parseTypeScriptFile(content);
121
- traverse.default(ast, {
144
+ traverse(ast, {
122
145
  ExportNamedDeclaration: (path) => {
123
146
  const declaration = path.node.declaration;
124
147
  if (t.isFunctionDeclaration(declaration) &&
125
148
  t.isIdentifier(declaration.id)) {
126
149
  const dataTypes = extractJSDocComments(path);
127
150
  if (this.isRoute(declaration.id.name)) {
151
+ const routePath = this.getRoutePath(filePath);
152
+ // Skip if route should be ignored
153
+ if (this.shouldIgnoreRoute(routePath, dataTypes)) {
154
+ logger.debug(`Ignoring route: ${routePath}`);
155
+ return;
156
+ }
128
157
  // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
129
158
  if (!this.config.includeOpenApiRoutes ||
130
159
  (this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
131
160
  // Check for URL parameters in the route path
132
- const routePath = this.getRoutePath(filePath);
133
161
  const pathParams = extractPathParameters(routePath);
134
162
  // If we have path parameters but no pathParamsType defined, we should log a warning
135
163
  if (pathParams.length > 0 && !dataTypes.pathParamsType) {
@@ -144,10 +172,15 @@ export class RouteProcessor {
144
172
  if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
145
173
  if (this.isRoute(decl.id.name)) {
146
174
  const dataTypes = extractJSDocComments(path);
175
+ const routePath = this.getRoutePath(filePath);
176
+ // Skip if route should be ignored
177
+ if (this.shouldIgnoreRoute(routePath, dataTypes)) {
178
+ logger.debug(`Ignoring route: ${routePath}`);
179
+ return;
180
+ }
147
181
  // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
148
182
  if (!this.config.includeOpenApiRoutes ||
149
183
  (this.config.includeOpenApiRoutes && dataTypes.isOpenApi)) {
150
- const routePath = this.getRoutePath(filePath);
151
184
  const pathParams = extractPathParameters(routePath);
152
185
  if (pathParams.length > 0 && !dataTypes.pathParamsType) {
153
186
  logger.debug(`Route ${routePath} contains path parameters ${pathParams.join(", ")} but no @pathParams type is defined.`);
@@ -243,7 +276,28 @@ export class RouteProcessor {
243
276
  }
244
277
  // Add request body
245
278
  if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
246
- definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
279
+ if (dataTypes.bodyType) {
280
+ // Ensure the schema is defined in components/schemas
281
+ this.schemaProcessor.getSchemaContent({
282
+ bodyType: dataTypes.bodyType,
283
+ });
284
+ // Use reference to the schema
285
+ const contentType = this.schemaProcessor.detectContentType(dataTypes.bodyType || "", dataTypes.contentType);
286
+ definition.requestBody = {
287
+ content: {
288
+ [contentType]: {
289
+ schema: { $ref: `#/components/schemas/${dataTypes.bodyType}` },
290
+ },
291
+ },
292
+ };
293
+ if (bodyDescription) {
294
+ definition.requestBody.description = bodyDescription;
295
+ }
296
+ }
297
+ else if (body && Object.keys(body).length > 0) {
298
+ // Fallback to inline schema for backward compatibility
299
+ definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
300
+ }
247
301
  }
248
302
  // Add responses
249
303
  definition.responses = this.buildResponsesFromConfig(dataTypes, method);
@@ -256,15 +310,15 @@ export class RouteProcessor {
256
310
  this.swaggerPaths[routePath][method] = definition;
257
311
  }
258
312
  getRoutePath(filePath) {
313
+ // Normalize path separators first
314
+ const normalizedPath = filePath.replaceAll("\\", "/");
259
315
  // First, check if it's an app router path
260
- if (filePath.includes("/app/api/")) {
316
+ if (normalizedPath.includes("/app/api/")) {
261
317
  // Get the relative path from the api directory
262
- const apiDirPos = filePath.indexOf("/app/api/");
263
- let relativePath = filePath.substring(apiDirPos + "/app/api".length);
318
+ const apiDirPos = normalizedPath.lastIndexOf("/app/api/");
319
+ let relativePath = normalizedPath.substring(apiDirPos + "/app/api".length);
264
320
  // Remove the /route.ts or /route.tsx suffix
265
321
  relativePath = relativePath.replace(/\/route\.tsx?$/, "");
266
- // Convert directory separators to URL path format
267
- relativePath = relativePath.replaceAll("\\", "/");
268
322
  // Remove Next.js route groups (folders in parentheses like (authenticated), (marketing))
269
323
  relativePath = relativePath.replace(/\/\([^)]+\)/g, "");
270
324
  // Convert Next.js dynamic route syntax to OpenAPI parameter syntax
@@ -274,10 +328,9 @@ export class RouteProcessor {
274
328
  return relativePath;
275
329
  }
276
330
  // For pages router or other formats
277
- const suffixPath = filePath.split("api")[1];
331
+ const suffixPath = normalizedPath.split("api")[1];
278
332
  return suffixPath
279
333
  .replace(/route\.tsx?$/, "")
280
- .replaceAll("\\", "/")
281
334
  .replace(/\/$/, "")
282
335
  .replace(/\/\([^)]+\)/g, "") // Remove route groups for pages router too
283
336
  .replace(/\/\[([^\]]+)\]/g, "/{$1}") // Replace [param] with {param}
@@ -1,7 +1,9 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import traverse from "@babel/traverse";
3
+ import traverseModule from "@babel/traverse";
4
4
  import * as t from "@babel/types";
5
+ // Handle both ES modules and CommonJS
6
+ const traverse = traverseModule.default || traverseModule;
5
7
  import { parseTypeScriptFile } from "./utils.js";
6
8
  import { ZodSchemaConverter } from "./zod-converter.js";
7
9
  import { logger } from "./logger.js";
@@ -28,20 +30,31 @@ export class SchemaProcessor {
28
30
  * Get all defined schemas (for components.schemas section)
29
31
  */
30
32
  getDefinedSchemas() {
33
+ // Filter out generic type parameters and invalid schema names
34
+ const filteredSchemas = {};
35
+ Object.entries(this.openapiDefinitions).forEach(([key, value]) => {
36
+ if (!this.isGenericTypeParameter(key) && !this.isInvalidSchemaName(key)) {
37
+ filteredSchemas[key] = value;
38
+ }
39
+ });
31
40
  // If using Zod, also include all processed Zod schemas
32
41
  if (this.schemaType === "zod" && this.zodSchemaConverter) {
33
42
  const zodSchemas = this.zodSchemaConverter.getProcessedSchemas();
34
43
  return {
35
- ...this.openapiDefinitions,
44
+ ...filteredSchemas,
36
45
  ...zodSchemas,
37
46
  };
38
47
  }
39
- return this.openapiDefinitions;
48
+ return filteredSchemas;
40
49
  }
41
50
  findSchemaDefinition(schemaName, contentType) {
42
51
  let schemaNode = null;
43
52
  // Assign type that is actually processed
44
53
  this.contentType = contentType;
54
+ // Check if the schemaName is a generic type (contains < and >)
55
+ if (schemaName.includes("<") && schemaName.includes(">")) {
56
+ return this.resolveGenericTypeFromString(schemaName);
57
+ }
45
58
  // Check if we should use Zod schemas
46
59
  if (this.schemaType === "zod") {
47
60
  logger.debug(`Looking for Zod schema: ${schemaName}`);
@@ -85,7 +98,7 @@ export class SchemaProcessor {
85
98
  });
86
99
  }
87
100
  collectTypeDefinitions(ast, schemaName) {
88
- traverse.default(ast, {
101
+ traverse(ast, {
89
102
  VariableDeclarator: (path) => {
90
103
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
91
104
  const name = path.node.id.name;
@@ -95,7 +108,14 @@ export class SchemaProcessor {
95
108
  TSTypeAliasDeclaration: (path) => {
96
109
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
97
110
  const name = path.node.id.name;
98
- this.typeDefinitions[name] = path.node.typeAnnotation;
111
+ // Store the full node for generic types, just the type annotation for regular types
112
+ if (path.node.typeParameters &&
113
+ path.node.typeParameters.params.length > 0) {
114
+ this.typeDefinitions[name] = path.node; // Store the full declaration for generic types
115
+ }
116
+ else {
117
+ this.typeDefinitions[name] = path.node.typeAnnotation; // Store just the type annotation for regular types
118
+ }
99
119
  }
100
120
  },
101
121
  TSInterfaceDeclaration: (path) => {
@@ -150,6 +170,13 @@ export class SchemaProcessor {
150
170
  const typeNode = this.typeDefinitions[typeName.toString()];
151
171
  if (!typeNode)
152
172
  return {};
173
+ // Handle generic type alias declarations (full node)
174
+ if (t.isTSTypeAliasDeclaration(typeNode)) {
175
+ // This is a generic type, should be handled by the caller via resolveGenericType
176
+ // For non-generic access, just return the type annotation
177
+ const typeAnnotation = typeNode.typeAnnotation;
178
+ return this.resolveTSNodeType(typeAnnotation);
179
+ }
153
180
  // Check if node is Zod
154
181
  if (t.isCallExpression(typeNode) &&
155
182
  t.isMemberExpression(typeNode.callee) &&
@@ -350,6 +377,16 @@ export class SchemaProcessor {
350
377
  return this.resolveTSNodeType(node.typeParameters.params[0]);
351
378
  }
352
379
  }
380
+ // Handle custom generic types
381
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
382
+ // Find the generic type definition first
383
+ this.findSchemaDefinition(typeName, this.contentType);
384
+ const genericTypeDefinition = this.typeDefinitions[typeName];
385
+ if (genericTypeDefinition) {
386
+ // Resolve the generic type by substituting type parameters
387
+ return this.resolveGenericType(genericTypeDefinition, node.typeParameters.params, typeName);
388
+ }
389
+ }
353
390
  // Check if it is a type that we are already processing
354
391
  if (this.processingTypes.has(typeName)) {
355
392
  return { $ref: `#/components/schemas/${typeName}` };
@@ -768,4 +805,275 @@ export class SchemaProcessor {
768
805
  responses,
769
806
  };
770
807
  }
808
+ /**
809
+ * Parse and resolve a generic type from a string like "MyApiSuccessResponseBody<LLMSResponse>"
810
+ * @param genericTypeString - The generic type string to parse and resolve
811
+ * @returns The resolved OpenAPI schema
812
+ */
813
+ resolveGenericTypeFromString(genericTypeString) {
814
+ // Parse the generic type string
815
+ const parsed = this.parseGenericTypeString(genericTypeString);
816
+ if (!parsed) {
817
+ return {};
818
+ }
819
+ const { baseTypeName, typeArguments } = parsed;
820
+ // Find the base generic type definition
821
+ this.scanSchemaDir(this.schemaDir, baseTypeName);
822
+ const genericTypeDefinition = this.typeDefinitions[baseTypeName];
823
+ if (!genericTypeDefinition) {
824
+ logger.debug(`Generic type definition not found for: ${baseTypeName}`);
825
+ return {};
826
+ }
827
+ // Also find all the type argument definitions
828
+ typeArguments.forEach((argTypeName) => {
829
+ // If it's a simple type reference (not another generic), find its definition
830
+ if (!argTypeName.includes("<") &&
831
+ !this.isGenericTypeParameter(argTypeName)) {
832
+ this.scanSchemaDir(this.schemaDir, argTypeName);
833
+ }
834
+ });
835
+ // Create AST nodes for the type arguments by parsing them
836
+ const typeArgumentNodes = typeArguments.map((arg) => this.createTypeNodeFromString(arg));
837
+ // Resolve the generic type
838
+ const resolved = this.resolveGenericType(genericTypeDefinition, typeArgumentNodes, baseTypeName);
839
+ // Cache the resolved type for future reference
840
+ this.openapiDefinitions[genericTypeString] = resolved;
841
+ return resolved;
842
+ }
843
+ /**
844
+ * Check if a type name is likely a generic type parameter (e.g., T, U, K, V)
845
+ * @param {string} typeName - The type name to check
846
+ * @returns {boolean} - True if it's likely a generic type parameter
847
+ */
848
+ isGenericTypeParameter(typeName) {
849
+ // Common generic type parameter patterns:
850
+ // - Single uppercase letters (T, U, K, V, etc.)
851
+ // - TKey, TValue, etc.
852
+ return /^[A-Z]$|^T[A-Z][a-zA-Z]*$/.test(typeName);
853
+ }
854
+ /**
855
+ * Check if a schema name is invalid (contains special characters, brackets, etc.)
856
+ * @param {string} schemaName - The schema name to check
857
+ * @returns {boolean} - True if the schema name is invalid
858
+ */
859
+ isInvalidSchemaName(schemaName) {
860
+ // Schema names should not contain { } : ? spaces or other special characters
861
+ return /[{}\s:?]/.test(schemaName);
862
+ }
863
+ /**
864
+ * Parse a generic type string into base type and arguments
865
+ * @param genericTypeString - The string like "MyApiSuccessResponseBody<LLMSResponse>"
866
+ * @returns Object with baseTypeName and typeArguments array
867
+ */
868
+ parseGenericTypeString(genericTypeString) {
869
+ const match = genericTypeString.match(/^([^<]+)<(.+)>$/);
870
+ if (!match) {
871
+ return null;
872
+ }
873
+ const baseTypeName = match[1].trim();
874
+ const typeArgsString = match[2].trim();
875
+ // Split type arguments by comma, handling nested generics
876
+ const typeArguments = this.splitTypeArguments(typeArgsString);
877
+ return { baseTypeName, typeArguments };
878
+ }
879
+ /**
880
+ * Split type arguments by comma, handling nested generics correctly
881
+ * @param typeArgsString - The string inside angle brackets
882
+ * @returns Array of individual type argument strings
883
+ */
884
+ splitTypeArguments(typeArgsString) {
885
+ const args = [];
886
+ let currentArg = "";
887
+ let bracketDepth = 0;
888
+ for (let i = 0; i < typeArgsString.length; i++) {
889
+ const char = typeArgsString[i];
890
+ if (char === "<") {
891
+ bracketDepth++;
892
+ }
893
+ else if (char === ">") {
894
+ bracketDepth--;
895
+ }
896
+ else if (char === "," && bracketDepth === 0) {
897
+ args.push(currentArg.trim());
898
+ currentArg = "";
899
+ continue;
900
+ }
901
+ currentArg += char;
902
+ }
903
+ if (currentArg.trim()) {
904
+ args.push(currentArg.trim());
905
+ }
906
+ return args;
907
+ }
908
+ /**
909
+ * Create a TypeScript AST node from a type string
910
+ * @param typeString - The type string like "LLMSResponse"
911
+ * @returns A TypeScript AST node
912
+ */
913
+ createTypeNodeFromString(typeString) {
914
+ // For simple type references, create a TSTypeReference node
915
+ if (!typeString.includes("<")) {
916
+ return {
917
+ type: "TSTypeReference",
918
+ typeName: {
919
+ type: "Identifier",
920
+ name: typeString,
921
+ },
922
+ };
923
+ }
924
+ // For nested generics, recursively parse
925
+ const parsed = this.parseGenericTypeString(typeString);
926
+ if (parsed) {
927
+ const typeParameterNodes = parsed.typeArguments.map((arg) => this.createTypeNodeFromString(arg));
928
+ return {
929
+ type: "TSTypeReference",
930
+ typeName: {
931
+ type: "Identifier",
932
+ name: parsed.baseTypeName,
933
+ },
934
+ typeParameters: {
935
+ type: "TSTypeParameterInstantiation",
936
+ params: typeParameterNodes,
937
+ },
938
+ };
939
+ }
940
+ // Fallback for unknown patterns
941
+ return {
942
+ type: "TSTypeReference",
943
+ typeName: {
944
+ type: "Identifier",
945
+ name: typeString,
946
+ },
947
+ };
948
+ }
949
+ /**
950
+ * Resolve generic types by substituting type parameters with actual types
951
+ * @param genericTypeDefinition - The AST node of the generic type definition
952
+ * @param typeArguments - The type arguments passed to the generic type
953
+ * @param typeName - The name of the generic type
954
+ * @returns The resolved OpenAPI schema
955
+ */
956
+ resolveGenericType(genericTypeDefinition, typeArguments, typeName) {
957
+ // Extract type parameters from the generic type definition
958
+ let typeParameters = [];
959
+ if (t.isTSTypeAliasDeclaration(genericTypeDefinition)) {
960
+ if (genericTypeDefinition.typeParameters &&
961
+ genericTypeDefinition.typeParameters.params) {
962
+ typeParameters = genericTypeDefinition.typeParameters.params.map((param) => {
963
+ if (t.isTSTypeParameter(param)) {
964
+ return param.name;
965
+ }
966
+ return t.isIdentifier(param)
967
+ ? param.name
968
+ : param.name?.name || param;
969
+ });
970
+ }
971
+ // Create a mapping from type parameters to actual types
972
+ const typeParameterMap = {};
973
+ typeParameters.forEach((param, index) => {
974
+ if (index < typeArguments.length) {
975
+ typeParameterMap[param] = typeArguments[index];
976
+ }
977
+ });
978
+ // Resolve the type annotation with substituted type parameters
979
+ return this.resolveTypeWithSubstitution(genericTypeDefinition.typeAnnotation, typeParameterMap);
980
+ }
981
+ // If we can't process the generic type, return empty object
982
+ return {};
983
+ }
984
+ /**
985
+ * Resolve a type node with type parameter substitution
986
+ * @param node - The AST node to resolve
987
+ * @param typeParameterMap - Mapping from type parameter names to actual types
988
+ * @returns The resolved OpenAPI schema
989
+ */
990
+ resolveTypeWithSubstitution(node, typeParameterMap) {
991
+ if (!node)
992
+ return { type: "object" };
993
+ // If this is a type parameter reference, substitute it
994
+ if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
995
+ const paramName = node.typeName.name;
996
+ if (typeParameterMap[paramName]) {
997
+ // The mapped value is an AST node, resolve it
998
+ const mappedNode = typeParameterMap[paramName];
999
+ if (t.isTSTypeReference(mappedNode) &&
1000
+ t.isIdentifier(mappedNode.typeName)) {
1001
+ // If it's a reference to another type, get the resolved schema from openapiDefinitions
1002
+ const referencedTypeName = mappedNode.typeName.name;
1003
+ if (this.openapiDefinitions[referencedTypeName]) {
1004
+ return this.openapiDefinitions[referencedTypeName];
1005
+ }
1006
+ // If not in openapiDefinitions, try to resolve it
1007
+ this.findSchemaDefinition(referencedTypeName, this.contentType);
1008
+ return this.openapiDefinitions[referencedTypeName] || {};
1009
+ }
1010
+ return this.resolveTSNodeType(typeParameterMap[paramName]);
1011
+ }
1012
+ }
1013
+ // Handle intersection types (e.g., T & { success: true })
1014
+ if (t.isTSIntersectionType(node)) {
1015
+ const allProperties = {};
1016
+ const requiredProperties = [];
1017
+ node.types.forEach((typeNode, index) => {
1018
+ let resolvedType;
1019
+ // Check if this is a type parameter reference
1020
+ if (t.isTSTypeReference(typeNode) &&
1021
+ t.isIdentifier(typeNode.typeName)) {
1022
+ const paramName = typeNode.typeName.name;
1023
+ if (typeParameterMap[paramName]) {
1024
+ const mappedNode = typeParameterMap[paramName];
1025
+ if (t.isTSTypeReference(mappedNode) &&
1026
+ t.isIdentifier(mappedNode.typeName)) {
1027
+ // If it's a reference to another type, get the resolved schema
1028
+ const referencedTypeName = mappedNode.typeName.name;
1029
+ if (this.openapiDefinitions[referencedTypeName]) {
1030
+ resolvedType = this.openapiDefinitions[referencedTypeName];
1031
+ }
1032
+ else {
1033
+ // If not in openapiDefinitions, try to resolve it
1034
+ this.findSchemaDefinition(referencedTypeName, this.contentType);
1035
+ resolvedType =
1036
+ this.openapiDefinitions[referencedTypeName] || {};
1037
+ }
1038
+ }
1039
+ else {
1040
+ resolvedType = this.resolveTSNodeType(mappedNode);
1041
+ }
1042
+ }
1043
+ else {
1044
+ resolvedType = this.resolveTSNodeType(typeNode);
1045
+ }
1046
+ }
1047
+ else {
1048
+ resolvedType = this.resolveTypeWithSubstitution(typeNode, typeParameterMap);
1049
+ }
1050
+ if (resolvedType.type === "object" && resolvedType.properties) {
1051
+ Object.entries(resolvedType.properties).forEach(([key, value]) => {
1052
+ allProperties[key] = value;
1053
+ if (value.required) {
1054
+ requiredProperties.push(key);
1055
+ }
1056
+ });
1057
+ }
1058
+ });
1059
+ return {
1060
+ type: "object",
1061
+ properties: allProperties,
1062
+ required: requiredProperties.length > 0 ? requiredProperties : undefined,
1063
+ };
1064
+ }
1065
+ // For other types, use the standard resolution but with parameter substitution
1066
+ if (t.isTSTypeLiteral(node)) {
1067
+ const properties = {};
1068
+ node.members.forEach((member) => {
1069
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
1070
+ const propName = member.key.name;
1071
+ properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
1072
+ }
1073
+ });
1074
+ return { type: "object", properties };
1075
+ }
1076
+ // Fallback to standard type resolution
1077
+ return this.resolveTSNodeType(node);
1078
+ }
771
1079
  }
package/dist/lib/utils.js CHANGED
@@ -25,6 +25,7 @@ export function extractJSDocComments(path) {
25
25
  let bodyType = "";
26
26
  let auth = "";
27
27
  let isOpenApi = false;
28
+ let isIgnored = false;
28
29
  let deprecated = false;
29
30
  let bodyDescription = "";
30
31
  let contentType = "";
@@ -37,6 +38,9 @@ export function extractJSDocComments(path) {
37
38
  comments.forEach((comment) => {
38
39
  const commentValue = cleanComment(comment.value);
39
40
  isOpenApi = commentValue.includes("@openapi");
41
+ if (commentValue.includes("@ignore")) {
42
+ isIgnored = true;
43
+ }
40
44
  if (commentValue.includes("@deprecated")) {
41
45
  deprecated = true;
42
46
  }
@@ -121,15 +125,12 @@ export function extractJSDocComments(path) {
121
125
  }
122
126
  }
123
127
  if (commentValue.includes("@response")) {
124
- const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?(\w+)(?::(.*))?/);
128
+ // Updated regex to support generic types
129
+ const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?([^@\n\r]+)(?:\s+(.*))?/);
125
130
  if (responseMatch) {
126
- const [, code, type, description] = responseMatch;
131
+ const [, code, type] = responseMatch;
127
132
  successCode = code || "";
128
- responseType = type;
129
- // Set responseDescription only if not already set by @responseDescription
130
- if (description?.trim() && !responseDescription) {
131
- responseDescription = description.trim();
132
- }
133
+ responseType = type?.trim();
133
134
  }
134
135
  else {
135
136
  responseType = extractTypeFromComment(commentValue, "@response");
@@ -146,6 +147,7 @@ export function extractJSDocComments(path) {
146
147
  pathParamsType,
147
148
  bodyType,
148
149
  isOpenApi,
150
+ isIgnored,
149
151
  deprecated,
150
152
  bodyDescription,
151
153
  contentType,
@@ -157,7 +159,10 @@ export function extractJSDocComments(path) {
157
159
  };
158
160
  }
159
161
  export function extractTypeFromComment(commentValue, tag) {
160
- return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
162
+ // Updated regex to support generic types with angle brackets
163
+ return (commentValue
164
+ .match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s]+)`))?.[1]
165
+ ?.trim() || "");
161
166
  }
162
167
  export function cleanComment(commentValue) {
163
168
  return commentValue.replace(/\*\s*/g, "").trim();
@@ -170,6 +175,7 @@ export function cleanSpec(spec) {
170
175
  "ui",
171
176
  "outputFile",
172
177
  "includeOpenApiRoutes",
178
+ "ignoreRoutes",
173
179
  "schemaType",
174
180
  "defaultResponseSet",
175
181
  "responseSets",
@@ -1,7 +1,9 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import traverse from "@babel/traverse";
3
+ import traverseModule from "@babel/traverse";
4
4
  import * as t from "@babel/types";
5
+ // Handle both ES modules and CommonJS
6
+ const traverse = traverseModule.default || traverseModule;
5
7
  import { parseTypeScriptFile } from "./utils.js";
6
8
  import { logger } from "./logger.js";
7
9
  /**
@@ -150,7 +152,7 @@ export class ZodSchemaConverter {
150
152
  // Create a map to store imported modules
151
153
  const importedModules = {};
152
154
  // Look for all exported Zod schemas
153
- traverse.default(ast, {
155
+ traverse(ast, {
154
156
  // Track imports for resolving local and imported schemas
155
157
  ImportDeclaration: (path) => {
156
158
  // Keep track of imports to resolve external schemas
@@ -457,7 +459,7 @@ export class ZodSchemaConverter {
457
459
  try {
458
460
  const content = fs.readFileSync(filePath, "utf-8");
459
461
  const ast = parseTypeScriptFile(content);
460
- traverse.default(ast, {
462
+ traverse(ast, {
461
463
  ExportNamedDeclaration: (path) => {
462
464
  if (t.isVariableDeclaration(path.node.declaration)) {
463
465
  path.node.declaration.declarations.forEach((declaration) => {
@@ -1353,7 +1355,7 @@ export class ZodSchemaConverter {
1353
1355
  try {
1354
1356
  const content = fs.readFileSync(filePath, "utf-8");
1355
1357
  const ast = parseTypeScriptFile(content);
1356
- traverse.default(ast, {
1358
+ traverse(ast, {
1357
1359
  TSTypeAliasDeclaration: (path) => {
1358
1360
  if (t.isIdentifier(path.node.id)) {
1359
1361
  const typeName = path.node.id.name;
@@ -1421,7 +1423,7 @@ export class ZodSchemaConverter {
1421
1423
  const content = fs.readFileSync(filePath, "utf-8");
1422
1424
  const ast = parseTypeScriptFile(content);
1423
1425
  // Collect all exported Zod schemas
1424
- traverse.default(ast, {
1426
+ traverse(ast, {
1425
1427
  ExportNamedDeclaration: (path) => {
1426
1428
  if (t.isVariableDeclaration(path.node.declaration)) {
1427
1429
  path.node.declaration.declarations.forEach((declaration) => {
@@ -93,5 +93,6 @@ export default {
93
93
  outputFile: "openapi.json",
94
94
  outputDir: "./public",
95
95
  includeOpenApiRoutes: false,
96
+ ignoreRoutes: [],
96
97
  debug: false,
97
98
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,8 +13,13 @@
13
13
  "dist"
14
14
  ],
15
15
  "scripts": {
16
- "build": "tsc",
17
- "prepare": "npm run build"
16
+ "clean": "rm -rf dist",
17
+ "build": "npm run clean && tsc",
18
+ "prepare": "npm run build",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "test:ui": "vitest --ui",
22
+ "test:coverage": "vitest run --coverage"
18
23
  },
19
24
  "repository": {
20
25
  "type": "git",
@@ -51,6 +56,8 @@
51
56
  },
52
57
  "devDependencies": {
53
58
  "@types/node": "^24.3.0",
54
- "typescript": "^5.9.2"
59
+ "@vitest/ui": "^3.2.4",
60
+ "typescript": "^5.9.2",
61
+ "vitest": "^3.2.4"
55
62
  }
56
63
  }