next-openapi-gen 0.7.7 → 0.7.9

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
@@ -554,6 +554,53 @@ export async function GET() {
554
554
 
555
555
  If no type/schema is provided for path parameters, a default schema will be generated.
556
556
 
557
+ ### TypeScript Generics Support
558
+
559
+ The library supports TypeScript generic types and automatically resolves them during documentation generation:
560
+
561
+ ```typescript
562
+ // src/app/api/llms/route.ts
563
+
564
+ import { NextResponse } from "next/server";
565
+
566
+ // Define generic response wrapper
567
+ type MyApiSuccessResponseBody<T> = T & {
568
+ success: true;
569
+ httpCode: string;
570
+ };
571
+
572
+ // Define specific response data
573
+ type LLMSResponse = {
574
+ llms: Array<{
575
+ id: string;
576
+ name: string;
577
+ provider: string;
578
+ isDefault: boolean;
579
+ }>;
580
+ };
581
+
582
+ /**
583
+ * Get list of available LLMs
584
+ * @description Get list of available LLMs with success wrapper
585
+ * @response 200:MyApiSuccessResponseBody<LLMSResponse>
586
+ * @openapi
587
+ */
588
+ export async function GET() {
589
+ return NextResponse.json({
590
+ success: true,
591
+ httpCode: "200",
592
+ llms: [
593
+ {
594
+ id: "gpt-5",
595
+ name: "GPT-5",
596
+ provider: "OpenAI",
597
+ isDefault: true,
598
+ },
599
+ ],
600
+ });
601
+ }
602
+ ```
603
+
557
604
  ### Intelligent Examples
558
605
 
559
606
  The library generates intelligent examples for parameters based on their name:
@@ -23,14 +23,15 @@ export class RouteProcessor {
23
23
  // 1. Add success response
24
24
  const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
25
25
  if (dataTypes.responseType) {
26
- const responseSchema = this.schemaProcessor.getSchemaContent({
26
+ // Ensure the schema is defined in components/schemas
27
+ this.schemaProcessor.getSchemaContent({
27
28
  responseType: dataTypes.responseType,
28
- }).responses;
29
+ });
29
30
  responses[successCode] = {
30
31
  description: dataTypes.responseDescription || "Successful response",
31
32
  content: {
32
33
  "application/json": {
33
- schema: responseSchema,
34
+ schema: { $ref: `#/components/schemas/${dataTypes.responseType}` },
34
35
  },
35
36
  },
36
37
  };
@@ -243,7 +244,28 @@ export class RouteProcessor {
243
244
  }
244
245
  // Add request body
245
246
  if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
246
- definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
247
+ if (dataTypes.bodyType) {
248
+ // Ensure the schema is defined in components/schemas
249
+ this.schemaProcessor.getSchemaContent({
250
+ bodyType: dataTypes.bodyType,
251
+ });
252
+ // Use reference to the schema
253
+ const contentType = this.schemaProcessor.detectContentType(dataTypes.bodyType || "", dataTypes.contentType);
254
+ definition.requestBody = {
255
+ content: {
256
+ [contentType]: {
257
+ schema: { $ref: `#/components/schemas/${dataTypes.bodyType}` },
258
+ },
259
+ },
260
+ };
261
+ if (bodyDescription) {
262
+ definition.requestBody.description = bodyDescription;
263
+ }
264
+ }
265
+ else if (body && Object.keys(body).length > 0) {
266
+ // Fallback to inline schema for backward compatibility
267
+ definition.requestBody = this.schemaProcessor.createRequestBodySchema(body, bodyDescription, dataTypes.contentType);
268
+ }
247
269
  }
248
270
  // Add responses
249
271
  definition.responses = this.buildResponsesFromConfig(dataTypes, method);
@@ -28,20 +28,31 @@ export class SchemaProcessor {
28
28
  * Get all defined schemas (for components.schemas section)
29
29
  */
30
30
  getDefinedSchemas() {
31
+ // Filter out generic type parameters and invalid schema names
32
+ const filteredSchemas = {};
33
+ Object.entries(this.openapiDefinitions).forEach(([key, value]) => {
34
+ if (!this.isGenericTypeParameter(key) && !this.isInvalidSchemaName(key)) {
35
+ filteredSchemas[key] = value;
36
+ }
37
+ });
31
38
  // If using Zod, also include all processed Zod schemas
32
39
  if (this.schemaType === "zod" && this.zodSchemaConverter) {
33
40
  const zodSchemas = this.zodSchemaConverter.getProcessedSchemas();
34
41
  return {
35
- ...this.openapiDefinitions,
42
+ ...filteredSchemas,
36
43
  ...zodSchemas,
37
44
  };
38
45
  }
39
- return this.openapiDefinitions;
46
+ return filteredSchemas;
40
47
  }
41
48
  findSchemaDefinition(schemaName, contentType) {
42
49
  let schemaNode = null;
43
50
  // Assign type that is actually processed
44
51
  this.contentType = contentType;
52
+ // Check if the schemaName is a generic type (contains < and >)
53
+ if (schemaName.includes("<") && schemaName.includes(">")) {
54
+ return this.resolveGenericTypeFromString(schemaName);
55
+ }
45
56
  // Check if we should use Zod schemas
46
57
  if (this.schemaType === "zod") {
47
58
  logger.debug(`Looking for Zod schema: ${schemaName}`);
@@ -95,7 +106,14 @@ export class SchemaProcessor {
95
106
  TSTypeAliasDeclaration: (path) => {
96
107
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
97
108
  const name = path.node.id.name;
98
- this.typeDefinitions[name] = path.node.typeAnnotation;
109
+ // Store the full node for generic types, just the type annotation for regular types
110
+ if (path.node.typeParameters &&
111
+ path.node.typeParameters.params.length > 0) {
112
+ this.typeDefinitions[name] = path.node; // Store the full declaration for generic types
113
+ }
114
+ else {
115
+ this.typeDefinitions[name] = path.node.typeAnnotation; // Store just the type annotation for regular types
116
+ }
99
117
  }
100
118
  },
101
119
  TSInterfaceDeclaration: (path) => {
@@ -150,6 +168,13 @@ export class SchemaProcessor {
150
168
  const typeNode = this.typeDefinitions[typeName.toString()];
151
169
  if (!typeNode)
152
170
  return {};
171
+ // Handle generic type alias declarations (full node)
172
+ if (t.isTSTypeAliasDeclaration(typeNode)) {
173
+ // This is a generic type, should be handled by the caller via resolveGenericType
174
+ // For non-generic access, just return the type annotation
175
+ const typeAnnotation = typeNode.typeAnnotation;
176
+ return this.resolveTSNodeType(typeAnnotation);
177
+ }
153
178
  // Check if node is Zod
154
179
  if (t.isCallExpression(typeNode) &&
155
180
  t.isMemberExpression(typeNode.callee) &&
@@ -350,6 +375,16 @@ export class SchemaProcessor {
350
375
  return this.resolveTSNodeType(node.typeParameters.params[0]);
351
376
  }
352
377
  }
378
+ // Handle custom generic types
379
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
380
+ // Find the generic type definition first
381
+ this.findSchemaDefinition(typeName, this.contentType);
382
+ const genericTypeDefinition = this.typeDefinitions[typeName];
383
+ if (genericTypeDefinition) {
384
+ // Resolve the generic type by substituting type parameters
385
+ return this.resolveGenericType(genericTypeDefinition, node.typeParameters.params, typeName);
386
+ }
387
+ }
353
388
  // Check if it is a type that we are already processing
354
389
  if (this.processingTypes.has(typeName)) {
355
390
  return { $ref: `#/components/schemas/${typeName}` };
@@ -768,4 +803,275 @@ export class SchemaProcessor {
768
803
  responses,
769
804
  };
770
805
  }
806
+ /**
807
+ * Parse and resolve a generic type from a string like "MyApiSuccessResponseBody<LLMSResponse>"
808
+ * @param genericTypeString - The generic type string to parse and resolve
809
+ * @returns The resolved OpenAPI schema
810
+ */
811
+ resolveGenericTypeFromString(genericTypeString) {
812
+ // Parse the generic type string
813
+ const parsed = this.parseGenericTypeString(genericTypeString);
814
+ if (!parsed) {
815
+ return {};
816
+ }
817
+ const { baseTypeName, typeArguments } = parsed;
818
+ // Find the base generic type definition
819
+ this.scanSchemaDir(this.schemaDir, baseTypeName);
820
+ const genericTypeDefinition = this.typeDefinitions[baseTypeName];
821
+ if (!genericTypeDefinition) {
822
+ logger.debug(`Generic type definition not found for: ${baseTypeName}`);
823
+ return {};
824
+ }
825
+ // Also find all the type argument definitions
826
+ typeArguments.forEach((argTypeName) => {
827
+ // If it's a simple type reference (not another generic), find its definition
828
+ if (!argTypeName.includes("<") &&
829
+ !this.isGenericTypeParameter(argTypeName)) {
830
+ this.scanSchemaDir(this.schemaDir, argTypeName);
831
+ }
832
+ });
833
+ // Create AST nodes for the type arguments by parsing them
834
+ const typeArgumentNodes = typeArguments.map((arg) => this.createTypeNodeFromString(arg));
835
+ // Resolve the generic type
836
+ const resolved = this.resolveGenericType(genericTypeDefinition, typeArgumentNodes, baseTypeName);
837
+ // Cache the resolved type for future reference
838
+ this.openapiDefinitions[genericTypeString] = resolved;
839
+ return resolved;
840
+ }
841
+ /**
842
+ * Check if a type name is likely a generic type parameter (e.g., T, U, K, V)
843
+ * @param {string} typeName - The type name to check
844
+ * @returns {boolean} - True if it's likely a generic type parameter
845
+ */
846
+ isGenericTypeParameter(typeName) {
847
+ // Common generic type parameter patterns:
848
+ // - Single uppercase letters (T, U, K, V, etc.)
849
+ // - TKey, TValue, etc.
850
+ return /^[A-Z]$|^T[A-Z][a-zA-Z]*$/.test(typeName);
851
+ }
852
+ /**
853
+ * Check if a schema name is invalid (contains special characters, brackets, etc.)
854
+ * @param {string} schemaName - The schema name to check
855
+ * @returns {boolean} - True if the schema name is invalid
856
+ */
857
+ isInvalidSchemaName(schemaName) {
858
+ // Schema names should not contain { } : ? spaces or other special characters
859
+ return /[{}\s:?]/.test(schemaName);
860
+ }
861
+ /**
862
+ * Parse a generic type string into base type and arguments
863
+ * @param genericTypeString - The string like "MyApiSuccessResponseBody<LLMSResponse>"
864
+ * @returns Object with baseTypeName and typeArguments array
865
+ */
866
+ parseGenericTypeString(genericTypeString) {
867
+ const match = genericTypeString.match(/^([^<]+)<(.+)>$/);
868
+ if (!match) {
869
+ return null;
870
+ }
871
+ const baseTypeName = match[1].trim();
872
+ const typeArgsString = match[2].trim();
873
+ // Split type arguments by comma, handling nested generics
874
+ const typeArguments = this.splitTypeArguments(typeArgsString);
875
+ return { baseTypeName, typeArguments };
876
+ }
877
+ /**
878
+ * Split type arguments by comma, handling nested generics correctly
879
+ * @param typeArgsString - The string inside angle brackets
880
+ * @returns Array of individual type argument strings
881
+ */
882
+ splitTypeArguments(typeArgsString) {
883
+ const args = [];
884
+ let currentArg = "";
885
+ let bracketDepth = 0;
886
+ for (let i = 0; i < typeArgsString.length; i++) {
887
+ const char = typeArgsString[i];
888
+ if (char === "<") {
889
+ bracketDepth++;
890
+ }
891
+ else if (char === ">") {
892
+ bracketDepth--;
893
+ }
894
+ else if (char === "," && bracketDepth === 0) {
895
+ args.push(currentArg.trim());
896
+ currentArg = "";
897
+ continue;
898
+ }
899
+ currentArg += char;
900
+ }
901
+ if (currentArg.trim()) {
902
+ args.push(currentArg.trim());
903
+ }
904
+ return args;
905
+ }
906
+ /**
907
+ * Create a TypeScript AST node from a type string
908
+ * @param typeString - The type string like "LLMSResponse"
909
+ * @returns A TypeScript AST node
910
+ */
911
+ createTypeNodeFromString(typeString) {
912
+ // For simple type references, create a TSTypeReference node
913
+ if (!typeString.includes("<")) {
914
+ return {
915
+ type: "TSTypeReference",
916
+ typeName: {
917
+ type: "Identifier",
918
+ name: typeString,
919
+ },
920
+ };
921
+ }
922
+ // For nested generics, recursively parse
923
+ const parsed = this.parseGenericTypeString(typeString);
924
+ if (parsed) {
925
+ const typeParameterNodes = parsed.typeArguments.map((arg) => this.createTypeNodeFromString(arg));
926
+ return {
927
+ type: "TSTypeReference",
928
+ typeName: {
929
+ type: "Identifier",
930
+ name: parsed.baseTypeName,
931
+ },
932
+ typeParameters: {
933
+ type: "TSTypeParameterInstantiation",
934
+ params: typeParameterNodes,
935
+ },
936
+ };
937
+ }
938
+ // Fallback for unknown patterns
939
+ return {
940
+ type: "TSTypeReference",
941
+ typeName: {
942
+ type: "Identifier",
943
+ name: typeString,
944
+ },
945
+ };
946
+ }
947
+ /**
948
+ * Resolve generic types by substituting type parameters with actual types
949
+ * @param genericTypeDefinition - The AST node of the generic type definition
950
+ * @param typeArguments - The type arguments passed to the generic type
951
+ * @param typeName - The name of the generic type
952
+ * @returns The resolved OpenAPI schema
953
+ */
954
+ resolveGenericType(genericTypeDefinition, typeArguments, typeName) {
955
+ // Extract type parameters from the generic type definition
956
+ let typeParameters = [];
957
+ if (t.isTSTypeAliasDeclaration(genericTypeDefinition)) {
958
+ if (genericTypeDefinition.typeParameters &&
959
+ genericTypeDefinition.typeParameters.params) {
960
+ typeParameters = genericTypeDefinition.typeParameters.params.map((param) => {
961
+ if (t.isTSTypeParameter(param)) {
962
+ return param.name;
963
+ }
964
+ return t.isIdentifier(param)
965
+ ? param.name
966
+ : param.name?.name || param;
967
+ });
968
+ }
969
+ // Create a mapping from type parameters to actual types
970
+ const typeParameterMap = {};
971
+ typeParameters.forEach((param, index) => {
972
+ if (index < typeArguments.length) {
973
+ typeParameterMap[param] = typeArguments[index];
974
+ }
975
+ });
976
+ // Resolve the type annotation with substituted type parameters
977
+ return this.resolveTypeWithSubstitution(genericTypeDefinition.typeAnnotation, typeParameterMap);
978
+ }
979
+ // If we can't process the generic type, return empty object
980
+ return {};
981
+ }
982
+ /**
983
+ * Resolve a type node with type parameter substitution
984
+ * @param node - The AST node to resolve
985
+ * @param typeParameterMap - Mapping from type parameter names to actual types
986
+ * @returns The resolved OpenAPI schema
987
+ */
988
+ resolveTypeWithSubstitution(node, typeParameterMap) {
989
+ if (!node)
990
+ return { type: "object" };
991
+ // If this is a type parameter reference, substitute it
992
+ if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
993
+ const paramName = node.typeName.name;
994
+ if (typeParameterMap[paramName]) {
995
+ // The mapped value is an AST node, resolve it
996
+ const mappedNode = typeParameterMap[paramName];
997
+ if (t.isTSTypeReference(mappedNode) &&
998
+ t.isIdentifier(mappedNode.typeName)) {
999
+ // If it's a reference to another type, get the resolved schema from openapiDefinitions
1000
+ const referencedTypeName = mappedNode.typeName.name;
1001
+ if (this.openapiDefinitions[referencedTypeName]) {
1002
+ return this.openapiDefinitions[referencedTypeName];
1003
+ }
1004
+ // If not in openapiDefinitions, try to resolve it
1005
+ this.findSchemaDefinition(referencedTypeName, this.contentType);
1006
+ return this.openapiDefinitions[referencedTypeName] || {};
1007
+ }
1008
+ return this.resolveTSNodeType(typeParameterMap[paramName]);
1009
+ }
1010
+ }
1011
+ // Handle intersection types (e.g., T & { success: true })
1012
+ if (t.isTSIntersectionType(node)) {
1013
+ const allProperties = {};
1014
+ const requiredProperties = [];
1015
+ node.types.forEach((typeNode, index) => {
1016
+ let resolvedType;
1017
+ // Check if this is a type parameter reference
1018
+ if (t.isTSTypeReference(typeNode) &&
1019
+ t.isIdentifier(typeNode.typeName)) {
1020
+ const paramName = typeNode.typeName.name;
1021
+ if (typeParameterMap[paramName]) {
1022
+ const mappedNode = typeParameterMap[paramName];
1023
+ if (t.isTSTypeReference(mappedNode) &&
1024
+ t.isIdentifier(mappedNode.typeName)) {
1025
+ // If it's a reference to another type, get the resolved schema
1026
+ const referencedTypeName = mappedNode.typeName.name;
1027
+ if (this.openapiDefinitions[referencedTypeName]) {
1028
+ resolvedType = this.openapiDefinitions[referencedTypeName];
1029
+ }
1030
+ else {
1031
+ // If not in openapiDefinitions, try to resolve it
1032
+ this.findSchemaDefinition(referencedTypeName, this.contentType);
1033
+ resolvedType =
1034
+ this.openapiDefinitions[referencedTypeName] || {};
1035
+ }
1036
+ }
1037
+ else {
1038
+ resolvedType = this.resolveTSNodeType(mappedNode);
1039
+ }
1040
+ }
1041
+ else {
1042
+ resolvedType = this.resolveTSNodeType(typeNode);
1043
+ }
1044
+ }
1045
+ else {
1046
+ resolvedType = this.resolveTypeWithSubstitution(typeNode, typeParameterMap);
1047
+ }
1048
+ if (resolvedType.type === "object" && resolvedType.properties) {
1049
+ Object.entries(resolvedType.properties).forEach(([key, value]) => {
1050
+ allProperties[key] = value;
1051
+ if (value.required) {
1052
+ requiredProperties.push(key);
1053
+ }
1054
+ });
1055
+ }
1056
+ });
1057
+ return {
1058
+ type: "object",
1059
+ properties: allProperties,
1060
+ required: requiredProperties.length > 0 ? requiredProperties : undefined,
1061
+ };
1062
+ }
1063
+ // For other types, use the standard resolution but with parameter substitution
1064
+ if (t.isTSTypeLiteral(node)) {
1065
+ const properties = {};
1066
+ node.members.forEach((member) => {
1067
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
1068
+ const propName = member.key.name;
1069
+ properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
1070
+ }
1071
+ });
1072
+ return { type: "object", properties };
1073
+ }
1074
+ // Fallback to standard type resolution
1075
+ return this.resolveTSNodeType(node);
1076
+ }
771
1077
  }
package/dist/lib/utils.js CHANGED
@@ -48,7 +48,11 @@ export function extractJSDocComments(path) {
48
48
  }
49
49
  }
50
50
  if (!summary) {
51
- summary = commentValue.split("\n")[0];
51
+ const firstLine = commentValue.split("\n")[0];
52
+ // Don't use tags as summary - only use actual descriptions
53
+ if (!firstLine.trim().startsWith("@")) {
54
+ summary = firstLine;
55
+ }
52
56
  }
53
57
  if (commentValue.includes("@auth")) {
54
58
  const regex = /@auth\s*(.*)/;
@@ -117,15 +121,12 @@ export function extractJSDocComments(path) {
117
121
  }
118
122
  }
119
123
  if (commentValue.includes("@response")) {
120
- const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?(\w+)(?::(.*))?/);
124
+ // Updated regex to support generic types
125
+ const responseMatch = commentValue.match(/@response\s+(?:(\d+):)?([^@\n\r]+)(?:\s+(.*))?/);
121
126
  if (responseMatch) {
122
- const [, code, type, description] = responseMatch;
127
+ const [, code, type] = responseMatch;
123
128
  successCode = code || "";
124
- responseType = type;
125
- // Set responseDescription only if not already set by @responseDescription
126
- if (description?.trim() && !responseDescription) {
127
- responseDescription = description.trim();
128
- }
129
+ responseType = type?.trim();
129
130
  }
130
131
  else {
131
132
  responseType = extractTypeFromComment(commentValue, "@response");
@@ -153,7 +154,10 @@ export function extractJSDocComments(path) {
153
154
  };
154
155
  }
155
156
  export function extractTypeFromComment(commentValue, tag) {
156
- return commentValue.match(new RegExp(`${tag}\\s*\\s*(\\w+)`))?.[1] || "";
157
+ // Updated regex to support generic types with angle brackets
158
+ return (commentValue
159
+ .match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s]+)`))?.[1]
160
+ ?.trim() || "");
157
161
  }
158
162
  export function cleanComment(commentValue) {
159
163
  return commentValue.replace(/\*\s*/g, "").trim();
@@ -855,22 +855,6 @@ export class ZodSchemaConverter {
855
855
  !t.isIdentifier(node.callee.property)) {
856
856
  return { type: "string" };
857
857
  }
858
- if (t.isMemberExpression(node.callee) &&
859
- t.isIdentifier(node.callee.property)) {
860
- const zodType = node.callee.property.name;
861
- // Custom() support for FormData
862
- if (zodType === "custom" && node.arguments.length > 0) {
863
- // Check if it is FormData
864
- if (t.isArrowFunctionExpression(node.arguments[0])) {
865
- // Assume custom FormData validation
866
- return {
867
- type: "object",
868
- additionalProperties: true,
869
- description: "Form data object",
870
- };
871
- }
872
- }
873
- }
874
858
  const zodType = node.callee.property.name;
875
859
  let schema = {};
876
860
  // Basic type mapping
@@ -990,6 +974,37 @@ export class ZodSchemaConverter {
990
974
  schema = { type: "object" };
991
975
  }
992
976
  break;
977
+ case "custom":
978
+ // Check if it has TypeScript generic type parameters (z.custom<File>())
979
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
980
+ const typeParam = node.typeParameters.params[0];
981
+ // Check if the generic type is File
982
+ if (t.isTSTypeReference(typeParam) &&
983
+ t.isIdentifier(typeParam.typeName) &&
984
+ typeParam.typeName.name === "File") {
985
+ schema = {
986
+ type: "string",
987
+ format: "binary",
988
+ };
989
+ }
990
+ else {
991
+ // Other generic types default to string
992
+ schema = { type: "string" };
993
+ }
994
+ }
995
+ else if (node.arguments.length > 0 &&
996
+ t.isArrowFunctionExpression(node.arguments[0])) {
997
+ // Legacy support: FormData validation
998
+ schema = {
999
+ type: "object",
1000
+ additionalProperties: true,
1001
+ };
1002
+ }
1003
+ else {
1004
+ // Default case for z.custom() without specific type detection
1005
+ schema = { type: "string" };
1006
+ }
1007
+ break;
993
1008
  default:
994
1009
  schema = { type: "string" };
995
1010
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
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",