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 +105 -13
- package/dist/lib/openapi-generator.js +2 -1
- package/dist/lib/route-processor.js +68 -15
- package/dist/lib/schema-processor.js +313 -5
- package/dist/lib/utils.js +14 -8
- package/dist/lib/zod-converter.js +7 -5
- package/dist/openapi-template.js +1 -0
- package/package.json +11 -4
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
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
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
|
|
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
|
-
|
|
28
|
+
// Ensure the schema is defined in components/schemas
|
|
29
|
+
this.schemaProcessor.getSchemaContent({
|
|
27
30
|
responseType: dataTypes.responseType,
|
|
28
|
-
})
|
|
31
|
+
});
|
|
29
32
|
responses[successCode] = {
|
|
30
33
|
description: dataTypes.responseDescription || "Successful response",
|
|
31
34
|
content: {
|
|
32
35
|
"application/json": {
|
|
33
|
-
schema:
|
|
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
|
|
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
|
-
|
|
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 (
|
|
316
|
+
if (normalizedPath.includes("/app/api/")) {
|
|
261
317
|
// Get the relative path from the api directory
|
|
262
|
-
const apiDirPos =
|
|
263
|
-
let relativePath =
|
|
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 =
|
|
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
|
|
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
|
-
...
|
|
44
|
+
...filteredSchemas,
|
|
36
45
|
...zodSchemas,
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1426
|
+
traverse(ast, {
|
|
1425
1427
|
ExportNamedDeclaration: (path) => {
|
|
1426
1428
|
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
1427
1429
|
path.node.declaration.declarations.forEach((declaration) => {
|
package/dist/openapi-template.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-openapi-gen",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
"
|
|
17
|
-
"
|
|
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
|
-
"
|
|
59
|
+
"@vitest/ui": "^3.2.4",
|
|
60
|
+
"typescript": "^5.9.2",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
55
62
|
}
|
|
56
63
|
}
|