next-openapi-gen 0.1.2 → 0.2.0

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.
@@ -1,145 +1,173 @@
1
- import * as t from "@babel/types";
2
- import fs from "fs";
3
- import path from "path";
4
- import traverse from "@babel/traverse";
5
- import { parse } from "@babel/parser";
6
- import { SchemaProcessor } from "./schema-processor.js";
7
- import { capitalize, extractJSDocComments, getOperationId } from "./utils.js";
8
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
9
- const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
10
- export class RouteProcessor {
11
- swaggerPaths = {};
12
- schemaProcessor;
13
- config;
14
- constructor(config) {
15
- this.config = config;
16
- this.schemaProcessor = new SchemaProcessor(config.schemaDir);
17
- }
18
- isRoute(varName) {
19
- return HTTP_METHODS.includes(varName);
20
- }
21
- processFile(filePath) {
22
- const content = fs.readFileSync(filePath, "utf-8");
23
- const ast = parse(content, {
24
- sourceType: "module",
25
- plugins: ["typescript", "decorators-legacy"],
26
- });
27
- traverse.default(ast, {
28
- ExportNamedDeclaration: (path) => {
29
- const declaration = path.node.declaration;
30
- if (t.isFunctionDeclaration(declaration) &&
31
- t.isIdentifier(declaration.id)) {
32
- const dataTypes = extractJSDocComments(path);
33
- if (this.isRoute(declaration.id.name)) {
34
- this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
35
- }
36
- }
37
- if (t.isVariableDeclaration(declaration)) {
38
- declaration.declarations.forEach((decl) => {
39
- if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
40
- if (this.isRoute(decl.id.name)) {
41
- this.addRouteToPaths(decl.id.name, filePath, extractJSDocComments(path));
42
- }
43
- }
44
- });
45
- }
46
- },
47
- });
48
- }
49
- scanApiRoutes(dir) {
50
- const files = fs.readdirSync(dir);
51
- files.forEach((file) => {
52
- const filePath = path.join(dir, file);
53
- const stat = fs.statSync(filePath);
54
- if (stat.isDirectory()) {
55
- this.scanApiRoutes(filePath);
56
- // @ts-ignore
57
- }
58
- else if (file.endsWith(".ts")) {
59
- this.processFile(filePath);
60
- }
61
- });
62
- }
63
- addRouteToPaths(varName, filePath, dataTypes) {
64
- const method = varName.toLowerCase();
65
- const routePath = this.getRoutePath(filePath);
66
- const rootPath = capitalize(routePath.split("/")[1]);
67
- const operationId = getOperationId(routePath, method);
68
- const { summary, description, auth, isOpenApi } = dataTypes;
69
- if (this.config.includeOpenApiRoutes && !isOpenApi) {
70
- // If flag is enabled and there is no @openapi tag, then skip path
71
- return;
72
- }
73
- if (!this.swaggerPaths[routePath]) {
74
- this.swaggerPaths[routePath] = {};
75
- }
76
- const { params, body, responses } = this.schemaProcessor.getSchemaContent(dataTypes);
77
- const definition = {
78
- operationId: operationId,
79
- summary: summary,
80
- description: description,
81
- tags: [rootPath],
82
- };
83
- // Add auth
84
- if (auth) {
85
- definition.security = [
86
- {
87
- [auth]: [],
88
- },
89
- ];
90
- }
91
- if (params) {
92
- definition.parameters =
93
- this.schemaProcessor.createRequestParamsSchema(params);
94
- }
95
- // Add request body
96
- if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
97
- definition.requestBody =
98
- this.schemaProcessor.createRequestBodySchema(body);
99
- }
100
- // Add responses
101
- definition.responses = responses
102
- ? this.schemaProcessor.createResponseSchema(responses)
103
- : {};
104
- this.swaggerPaths[routePath][method] = definition;
105
- }
106
- getRoutePath(filePath) {
107
- const suffixPath = filePath.split("api")[1];
108
- return suffixPath
109
- .replace("route.ts", "")
110
- .replaceAll("\\", "/")
111
- .replace(/\/$/, "");
112
- }
113
- getSortedPaths(paths) {
114
- function comparePaths(a, b) {
115
- const aMethods = this.swaggerPaths[a] || {};
116
- const bMethods = this.swaggerPaths[b] || {};
117
- // Extract tags for all methods in path a
118
- const aTags = Object.values(aMethods).flatMap((method) => method.tags || []);
119
- // Extract tags for all methods in path b
120
- const bTags = Object.values(bMethods).flatMap((method) => method.tags || []);
121
- // Let's user only the first tags
122
- const aPrimaryTag = aTags[0] || "";
123
- const bPrimaryTag = bTags[0] || "";
124
- // Sort alphabetically based on the first tag
125
- const tagComparison = aPrimaryTag.localeCompare(bPrimaryTag);
126
- if (tagComparison !== 0) {
127
- return tagComparison; // Return the result of tag comparison
128
- }
129
- // Compare lengths of the paths
130
- const aLength = a.split("/").length;
131
- const bLength = b.split("/").length;
132
- return aLength - bLength; // Shorter paths come before longer ones
133
- }
134
- return Object.keys(paths)
135
- .sort(comparePaths.bind(this))
136
- .reduce((sorted, key) => {
137
- sorted[key] = paths[key];
138
- return sorted;
139
- }, {});
140
- }
141
- getSwaggerPaths() {
142
- const paths = this.getSortedPaths(this.swaggerPaths);
143
- return this.getSortedPaths(paths);
144
- }
145
- }
1
+ import * as t from "@babel/types";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import traverse from "@babel/traverse";
5
+ import { parse } from "@babel/parser";
6
+ import { SchemaProcessor } from "./schema-processor.js";
7
+ import { capitalize, extractJSDocComments, getOperationId } from "./utils.js";
8
+ const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
9
+ const MUTATION_HTTP_METHODS = ["PATCH", "POST", "PUT"];
10
+ export class RouteProcessor {
11
+ swaggerPaths = {};
12
+ schemaProcessor;
13
+ config;
14
+ directoryCache = {};
15
+ statCache = {};
16
+ processFileTracker = {};
17
+ constructor(config) {
18
+ this.config = config;
19
+ this.schemaProcessor = new SchemaProcessor(config.schemaDir);
20
+ }
21
+ isRoute(varName) {
22
+ return HTTP_METHODS.includes(varName);
23
+ }
24
+ processFile(filePath) {
25
+ // Check if the file has already been processed
26
+ if (this.processFileTracker[filePath])
27
+ return;
28
+ const content = fs.readFileSync(filePath, "utf-8");
29
+ const ast = parse(content, {
30
+ sourceType: "module",
31
+ plugins: ["typescript", "decorators-legacy"],
32
+ });
33
+ traverse.default(ast, {
34
+ ExportNamedDeclaration: (path) => {
35
+ const declaration = path.node.declaration;
36
+ if (t.isFunctionDeclaration(declaration) &&
37
+ t.isIdentifier(declaration.id)) {
38
+ const dataTypes = extractJSDocComments(path);
39
+ if (this.isRoute(declaration.id.name)) {
40
+ // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
41
+ if (!this.config.includeOpenApiRoutes || (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
42
+ this.addRouteToPaths(declaration.id.name, filePath, dataTypes);
43
+ }
44
+ }
45
+ if (t.isVariableDeclaration(declaration)) {
46
+ declaration.declarations.forEach((decl) => {
47
+ if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) {
48
+ if (this.isRoute(decl.id.name)) {
49
+ const dataTypes = extractJSDocComments(path);
50
+ // Don't bother adding routes for processing if only including OpenAPI routes and the route is not OpenAPI
51
+ if (!this.config.includeOpenApiRoutes || (this.config.includeOpenApiRoutes && dataTypes.isOpenApi))
52
+ this.addRouteToPaths(decl.id.name, filePath, dataTypes);
53
+ }
54
+ }
55
+ });
56
+ }
57
+ },
58
+ });
59
+ this.processFileTracker[filePath] = true;
60
+ }
61
+ scanApiRoutes(dir) {
62
+ let files = this.directoryCache[dir];
63
+ if (!files) {
64
+ files = fs.readdirSync(dir);
65
+ this.directoryCache[dir] = files;
66
+ }
67
+ files.forEach((file) => {
68
+ const filePath = path.join(dir, file);
69
+ let stat = this.statCache[filePath];
70
+ if (!stat) {
71
+ stat = fs.statSync(filePath);
72
+ this.statCache[filePath] = stat;
73
+ }
74
+ if (stat.isDirectory()) {
75
+ this.scanApiRoutes(filePath);
76
+ // @ts-ignore
77
+ }
78
+ else if (file.endsWith(".ts")) {
79
+ this.processFile(filePath);
80
+ }
81
+ });
82
+ }
83
+ addRouteToPaths(varName, filePath, dataTypes) {
84
+ const method = varName.toLowerCase();
85
+ const routePath = this.getRoutePath(filePath);
86
+ const rootPath = capitalize(routePath.split("/")[1]);
87
+ const operationId = getOperationId(routePath, method);
88
+ const { summary, description, auth, isOpenApi } = dataTypes;
89
+ if (this.config.includeOpenApiRoutes && !isOpenApi) {
90
+ // If flag is enabled and there is no @openapi tag, then skip path
91
+ return;
92
+ }
93
+ if (!this.swaggerPaths[routePath]) {
94
+ this.swaggerPaths[routePath] = {};
95
+ }
96
+ const { params, pathParams, body, responses } = this.schemaProcessor.getSchemaContent(dataTypes);
97
+ const definition = {
98
+ operationId: operationId,
99
+ summary: summary,
100
+ description: description,
101
+ tags: [rootPath],
102
+ };
103
+ // Add auth
104
+ if (auth) {
105
+ definition.security = [
106
+ {
107
+ [auth]: [],
108
+ },
109
+ ];
110
+ }
111
+ definition.parameters = [];
112
+ if (params) {
113
+ definition.parameters =
114
+ this.schemaProcessor.createRequestParamsSchema(params);
115
+ }
116
+ if (pathParams) {
117
+ const moreParams = this.schemaProcessor.createRequestParamsSchema(pathParams, true);
118
+ definition.parameters.push(...moreParams);
119
+ }
120
+ // Add request body
121
+ if (MUTATION_HTTP_METHODS.includes(method.toUpperCase())) {
122
+ definition.requestBody =
123
+ this.schemaProcessor.createRequestBodySchema(body);
124
+ }
125
+ // Add responses
126
+ definition.responses = responses
127
+ ? this.schemaProcessor.createResponseSchema(responses)
128
+ : {};
129
+ this.swaggerPaths[routePath][method] = definition;
130
+ }
131
+ getRoutePath(filePath) {
132
+ const suffixPath = filePath.split("api")[1];
133
+ return suffixPath
134
+ .replace("route.ts", "")
135
+ .replaceAll("\\", "/")
136
+ .replace(/\/$/, "")
137
+ // Turns NextJS-style dynamic routes into OpenAPI-style dynamic routes
138
+ .replaceAll("[", "{")
139
+ .replaceAll("]", "}");
140
+ }
141
+ getSortedPaths(paths) {
142
+ function comparePaths(a, b) {
143
+ const aMethods = this.swaggerPaths[a] || {};
144
+ const bMethods = this.swaggerPaths[b] || {};
145
+ // Extract tags for all methods in path a
146
+ const aTags = Object.values(aMethods).flatMap((method) => method.tags || []);
147
+ // Extract tags for all methods in path b
148
+ const bTags = Object.values(bMethods).flatMap((method) => method.tags || []);
149
+ // Let's user only the first tags
150
+ const aPrimaryTag = aTags[0] || "";
151
+ const bPrimaryTag = bTags[0] || "";
152
+ // Sort alphabetically based on the first tag
153
+ const tagComparison = aPrimaryTag.localeCompare(bPrimaryTag);
154
+ if (tagComparison !== 0) {
155
+ return tagComparison; // Return the result of tag comparison
156
+ }
157
+ // Compare lengths of the paths
158
+ const aLength = a.split("/").length;
159
+ const bLength = b.split("/").length;
160
+ return aLength - bLength; // Shorter paths come before longer ones
161
+ }
162
+ return Object.keys(paths)
163
+ .sort(comparePaths.bind(this))
164
+ .reduce((sorted, key) => {
165
+ sorted[key] = paths[key];
166
+ return sorted;
167
+ }, {});
168
+ }
169
+ getSwaggerPaths() {
170
+ const paths = this.getSortedPaths(this.swaggerPaths);
171
+ return this.getSortedPaths(paths);
172
+ }
173
+ }